about summary refs log blame commit diff stats
path: root/src/loader/response.nim
blob: dbec92d2c4e31a6bd031364f108ce67918163f1b (plain) (tree)
1
2
3
4
5
6
7
8
9
10

                   


                       
                      
               
                    

                     
                 
                

                      
 


                         
 
    















                                             
                        
                                                 
             
                       
                             
                             
                               
                              


                                      
                                                           
                  
 

                      
                                                                               




                                
                

   
                                                                   









                                 



                                                         

                                         

                      





                                            
                          
 
















                                                                        
                                                                      




                                          

                                          




                                                          
                                                          
                                                    

                                                                
                                
                         
         
                                              
   
 
                                                                    







                                                          
                                             

                                                        
                                                  


                             
                                                                       

             
                                                                     
                
                                                                        


                                                                 


                                         
import std/strutils
import std/tables

import bindings/quickjs
import io/promise
import io/socketstream
import js/error
import js/javascript
import loader/headers
import loader/request
import types/blob
import types/url
import utils/mimeguess
import utils/twtstr

import chagashi/charset
import chagashi/decoder
import chagashi/validator

type
  ResponseType* = enum
    TYPE_DEFAULT = "default"
    TYPE_BASIC = "basic"
    TYPE_CORS = "cors"
    TYPE_ERROR = "error"
    TYPE_OPAQUE = "opaque"
    TYPE_OPAQUEREDIRECT = "opaqueredirect"

  #TODO fully implement headers guards
  HeadersGuard* = enum
    GUARD_IMMUTABLE = "immutable"
    GUARD_REQUEST = "request"
    GUARD_REQUEST_NO_CORS = "request-no-cors"
    GUARD_RESPONSE = "response"
    GUARD_NONE = "none"

  Response* = ref object
    responseType* {.jsget: "type".}: ResponseType
    res*: int
    body*: SocketStream
    bodyUsed* {.jsget.}: bool
    status* {.jsget.}: uint16
    headers* {.jsget.}: Headers
    headersGuard: HeadersGuard
    url*: URL #TODO should be urllist?
    unregisterFun*: proc()
    bodyRead*: Promise[string]
    internalMessage*: string # should NOT be exposed to JS!
    outputId*: int

jsDestructor(Response)

proc newResponse*(res: int; request: Request; stream: SocketStream): Response =
  return Response(
    res: res,
    url: request.url,
    body: stream,
    bodyRead: Promise[string](),
    outputId: -1
  )

func makeNetworkError*(): Response {.jsstfunc: "Response:error".} =
  #TODO use "create" function
  #TODO headers immutable
  return Response(
    res: 0,
    responseType: TYPE_ERROR,
    status: 0,
    headers: newHeaders(),
    headersGuard: GUARD_IMMUTABLE
  )

func sok(response: Response): bool {.jsfget: "ok".} =
  return response.status in 200u16 .. 299u16

func surl(response: Response): string {.jsfget: "url".} =
  if response.responseType == TYPE_ERROR:
    return ""
  return $response.url

#TODO: this should be a property of body
proc close*(response: Response) {.jsfunc.} =
  response.bodyUsed = true
  if response.unregisterFun != nil:
    response.unregisterFun()
  if response.body != nil:
    response.body.sclose()

func getCharset*(this: Response; fallback: Charset): Charset =
  if "Content-Type" notin this.headers.table:
    return fallback
  let header = this.headers.table["Content-Type"][0].toLowerAscii()
  let cs = header.getContentTypeAttr("charset").getCharset()
  if cs == CHARSET_UNKNOWN:
    return fallback
  return cs

func getContentType*(this: Response): string =
  if "Content-Type" in this.headers.table:
    let header = this.headers.table["Content-Type"][0].toLowerAscii()
    return header.until(';').strip()
  # also use DefaultGuess for container, so that local mime.types cannot
  # override buffer mime.types
  return DefaultGuess.guessContentType(this.url.pathname)

proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} =
  if response.body == nil:
    let p = newPromise[JSResult[string]]()
    p.resolve(JSResult[string].ok(""))
    return p
  if response.bodyUsed:
    let p = newPromise[JSResult[string]]()
    let err = JSResult[string]
      .err(newTypeError("Body has already been consumed"))
    p.resolve(err)
    return p
  let bodyRead = response.bodyRead
  response.bodyRead = nil
  return bodyRead.then(proc(s: string): JSResult[string] =
    let charset = response.getCharset(CHARSET_UTF_8)
    #TODO this is inefficient
    # maybe add a JS type that turns a seq[char] into JS strings
    if charset == CHARSET_UTF_8:
      ok(s.toValidUTF8())
    else:
      ok(newTextDecoder(charset).decodeAll(s))
  )

proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} =
  if response.bodyRead == nil:
    let p = newPromise[JSResult[Blob]]()
    let err = JSResult[Blob]
      .err(newTypeError("Body has already been consumed"))
    p.resolve(err)
    return p
  let bodyRead = response.bodyRead
  response.bodyRead = nil
  let contentType = response.getContentType()
  return bodyRead.then(proc(s: string): JSResult[Blob] =
    if s.len == 0:
      return ok(newBlob(nil, 0, contentType, nil))
    GC_ref(s)
    let deallocFun = proc() =
      GC_unref(s)
    let blob = newBlob(unsafeAddr s[0], s.len, contentType, deallocFun)
    ok(blob))

proc json(ctx: JSContext, this: Response): Promise[JSResult[JSValue]]
    {.jsfunc.} =
  return this.text().then(proc(s: JSResult[string]): JSResult[JSValue] =
    let s = ?s
    return ok(JS_ParseJSON(ctx, cstring(s), cast[csize_t](s.len),
      cstring"<input>")))

proc addResponseModule*(ctx: JSContext) =
  ctx.registerType(Response)