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)
|