about summary refs log tree commit diff stats
path: root/src/loader/response.nim
blob: 6b4ec64e8a2c1bbb9df1079273adfb0f55fc92c6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import std/streams
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.close()

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)