|
| 1 | +interface ResponseInit { |
| 2 | + headers?: HeadersInit; |
| 3 | + status?: number; |
| 4 | + statusText?: string; |
| 5 | +} |
| 6 | + |
| 7 | +class Response { |
| 8 | + #response; |
| 9 | + #headers: any; |
| 10 | + /** |
| 11 | + * The new Response(body, init) constructor steps are: |
| 12 | + * @see https://fetch.spec.whatwg.org/#dom-response |
| 13 | + */ |
| 14 | + constructor(body: any, init: ResponseInit) { |
| 15 | + // 1. Set this’s response to a new response. |
| 16 | + this.#response = makeResponse(init); |
| 17 | + |
| 18 | + // TODO: implement module |
| 19 | + // 2. Set this’s headers to a new Headers object with this’s relevant realm, whose header list is this’s response’s header list and guard is "response". |
| 20 | + |
| 21 | + // 3. Let bodyWithType be null. |
| 22 | + let bodyWithType = null; |
| 23 | + |
| 24 | + // 4. If body is non-null, then set bodyWithType to the result of extracting body. |
| 25 | + if (body != null) { |
| 26 | + const [extractedBody, type] = extractBody(body); |
| 27 | + bodyWithType = { body: extractedBody, type }; |
| 28 | + } |
| 29 | + // 5. Perform initialize a response given this, init, and bodyWithType. |
| 30 | + initializeAResponse(this, init, bodyWithType); |
| 31 | + } |
| 32 | + |
| 33 | + static getResponse(response: Response) { |
| 34 | + return response.#response; |
| 35 | + } |
| 36 | + |
| 37 | + /** The type getter steps are to return this’s response’s type. */ |
| 38 | + get type() { |
| 39 | + return this.#response.type; |
| 40 | + } |
| 41 | + |
| 42 | + /** |
| 43 | + * The url getter steps are to return the empty string if this’s response’s URL is null; |
| 44 | + * otherwise this’s response’s URL, serialized with exclude fragment set to true. |
| 45 | + */ |
| 46 | + get url() { |
| 47 | + return this.#response.url; |
| 48 | + } |
| 49 | + |
| 50 | + /** The redirected getter steps are to return true if this’s response’s URL list’s size is greater than 1; otherwise false. */ |
| 51 | + get redirected() { |
| 52 | + return this.#response.url.length > 1; |
| 53 | + } |
| 54 | + |
| 55 | + /** The status getter steps are to return this’s response’s status. */ |
| 56 | + get status() { |
| 57 | + return this.#response.status; |
| 58 | + } |
| 59 | + |
| 60 | + /** The ok getter steps are to return true if this’s response’s status is an ok status; otherwise false. */ |
| 61 | + get ok() { |
| 62 | + const status = this.#response.status; |
| 63 | + return status >= 200 && status <= 299; |
| 64 | + } |
| 65 | + |
| 66 | + /** The statusText getter steps are to return this’s response’s status message. */ |
| 67 | + get statusText() { |
| 68 | + return this.#response.statusText; |
| 69 | + } |
| 70 | + |
| 71 | + /** The headers getter steps are to return this’s headers. */ |
| 72 | + get headers() { |
| 73 | + return this.#headers; |
| 74 | + } |
| 75 | + |
| 76 | + // TODO |
| 77 | + get body() { |
| 78 | + return this.#response.body; |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +const { getResponse } = Response; |
| 83 | + |
| 84 | +// TODO: headers |
| 85 | +function makeResponse(init: ResponseInit) { |
| 86 | + return { |
| 87 | + aborted: false, |
| 88 | + rangeRequested: false, |
| 89 | + timingAllowPassed: false, |
| 90 | + requestIncludesCredentials: false, |
| 91 | + type: "default", |
| 92 | + status: 200, |
| 93 | + timingInfo: null, |
| 94 | + cacheState: "", |
| 95 | + statusText: "", |
| 96 | + url: "", |
| 97 | + body: null, |
| 98 | + ...init, |
| 99 | + }; |
| 100 | +} |
| 101 | + |
| 102 | +function initializeAResponse( |
| 103 | + response: Response, |
| 104 | + init: ResponseInit, |
| 105 | + body: { |
| 106 | + body: any; |
| 107 | + type: any; |
| 108 | + } | null, |
| 109 | +) { |
| 110 | + // 1. If init["status"] is not in the range 200 to 599, inclusive, then throw a RangeError. |
| 111 | + if ( |
| 112 | + init.status != null && (init.status < 200 || init.status > 599) |
| 113 | + ) { |
| 114 | + throw new RangeError( |
| 115 | + `The status provided (${init.status}) is not equal to 101 and outside the range [200, 599]`, |
| 116 | + ); |
| 117 | + } |
| 118 | + |
| 119 | + // 2. If init["statusText"] is not the empty string and does not match the reason-phrase token production, then throw a TypeError. |
| 120 | + // TODO: implement RegExp. |
| 121 | + if ( |
| 122 | + init.statusText && isValidReasonPhrase(init.statusText) |
| 123 | + ) { |
| 124 | + throw new TypeError( |
| 125 | + `Invalid status text: "${init.statusText}"`, |
| 126 | + ); |
| 127 | + } |
| 128 | + |
| 129 | + // 3. Set response’s response’s status to init["status"]. |
| 130 | + if (init.status != null) { |
| 131 | + getResponse(response).status = init.status; |
| 132 | + } |
| 133 | + |
| 134 | + // 4. Set response’s response’s status message to init["statusText"]. |
| 135 | + if (init.statusText != null) { |
| 136 | + getResponse(response).statusText = init.statusText; |
| 137 | + } |
| 138 | + |
| 139 | + // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. |
| 140 | + if (init.headers != null) { |
| 141 | + // TODO: get headerlist |
| 142 | + getResponse(response).headers = init.headers; |
| 143 | + } |
| 144 | + |
| 145 | + // 6. If body is non-null, then: |
| 146 | + if (body != null) { |
| 147 | + // 1. If response’s status is a null body status, then throw a TypeError. |
| 148 | + // NOTE: 101 and 103 are included in null body status due to their use elsewhere. They do not affect this step. |
| 149 | + if (nullBodyStatus(response.status)) { |
| 150 | + throw new TypeError( |
| 151 | + "Response with null body status cannot have body", |
| 152 | + ); |
| 153 | + } |
| 154 | + // 2. Set response’s body to body’s body. |
| 155 | + getResponse(response).body = body.body; |
| 156 | + // 3. If body’s type is non-null and response’s header list does not contain `Content-Type`, then append (`Content-Type`, body’s type) to response’s header list. |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +/** |
| 161 | + * TODO: when implemented module, move to ext/fetch/body |
| 162 | + * To extract a body with type from a byte sequence or BodyInit object object, |
| 163 | + * with an optional boolean keepalive (default false) |
| 164 | + * @see https://fetch.spec.whatwg.org/#concept-bodyinit-extract |
| 165 | + */ |
| 166 | +function extractBody(object: any, _keepalive = false) { |
| 167 | + // 1. Let stream be null. |
| 168 | + let stream = null; |
| 169 | + // 2. If object is a ReadableStream object, then set stream to object. |
| 170 | + // TODO: implement ReadableStream |
| 171 | + // 3. Otherwise, if object is a Blob object, set stream to the result of running object’s get stream. |
| 172 | + // 4. Otherwise, set stream to a new ReadableStream object, and set up stream with byte reading support. |
| 173 | + // 5. Assert: stream is a ReadableStream object. |
| 174 | + |
| 175 | + // 6, Let action be null. |
| 176 | + let _action = null; |
| 177 | + |
| 178 | + // 7. Let source be null. |
| 179 | + let source = null; |
| 180 | + |
| 181 | + // 8. Let length be null. |
| 182 | + let length = null; |
| 183 | + |
| 184 | + // Let type be null. |
| 185 | + let type = null; |
| 186 | + |
| 187 | + // Switch on object: |
| 188 | + if (typeof object == "string") { |
| 189 | + // scalar value string: |
| 190 | + // Set source to the UTF-8 encoding of object. |
| 191 | + // Set type to `text/plain;charset=UTF-8`. |
| 192 | + source = object; |
| 193 | + type = "text/plain;charset=UTF-8"; |
| 194 | + } else { |
| 195 | + console.error("TODO: these doesn't yet supported"); |
| 196 | + // Blob |
| 197 | + // Set source to object. |
| 198 | + // Set length to object’s size. |
| 199 | + // If object’s type attribute is not the empty byte sequence, set type to its value. |
| 200 | + |
| 201 | + // byte sequence: |
| 202 | + // Set source to object. |
| 203 | + |
| 204 | + // BufferSource: |
| 205 | + // Set source to a copy of the bytes held by object. |
| 206 | + |
| 207 | + // FormData: |
| 208 | + // Set action to this step: run the multipart/form-data encoding algorithm, with object’s entry list and UTF-8. |
| 209 | + |
| 210 | + // Set source to object. |
| 211 | + |
| 212 | + // Set length to unclear, see html/6424 for improving this. |
| 213 | + |
| 214 | + // Set type to `multipart/form-data; boundary=`, followed by the multipart/form-data boundary string generated by the multipart/form-data encoding algorithm. |
| 215 | + |
| 216 | + // URLSearchParams: |
| 217 | + // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list. |
| 218 | + |
| 219 | + // Set type to `application/x-www-form-urlencoded;charset=UTF-8`. |
| 220 | + |
| 221 | + // ReadableStream: |
| 222 | + // If keepalive is true, then throw a TypeError. |
| 223 | + // If object is disturbed or locked, then throw a TypeError. |
| 224 | + } |
| 225 | + |
| 226 | + // 11. If source is a byte sequence, then set action to a step that returns source and length to source’s length. |
| 227 | + |
| 228 | + // 12. If action is non-null, then run these steps in parallel: |
| 229 | + // 1. Run action. |
| 230 | + // Whenever one or more bytes are available and stream is not errored, enqueue the result of creating a Uint8Array from the available bytes into stream. |
| 231 | + // When running action is done, close stream. |
| 232 | + |
| 233 | + // 13. Let body be a body whose stream is stream, source is source, and length is length. |
| 234 | + const body = { stream, source, length }; |
| 235 | + |
| 236 | + // 14. Return (body, type). |
| 237 | + return [body, type]; |
| 238 | +} |
| 239 | + |
| 240 | +// Check whether |statusText| is a ByteString and |
| 241 | +// matches the Reason-Phrase token production. |
| 242 | +// RFC 2616: https://tools.ietf.org/html/rfc2616 |
| 243 | +// RFC 7230: https://tools.ietf.org/html/rfc7230 |
| 244 | +// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )" |
| 245 | +// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116 |
| 246 | +function isValidReasonPhrase(statusText: string) { |
| 247 | + for (let i = 0; i < statusText.length; ++i) { |
| 248 | + const c = statusText.charCodeAt(i); |
| 249 | + if ( |
| 250 | + !( |
| 251 | + c === 0x09 || // HTAB |
| 252 | + (c >= 0x20 && c <= 0x7e) || // SP / VCHAR |
| 253 | + (c >= 0x80 && c <= 0xff) // obs-text |
| 254 | + ) |
| 255 | + ) { |
| 256 | + return false; |
| 257 | + } |
| 258 | + } |
| 259 | + return true; |
| 260 | +} |
| 261 | + |
| 262 | +/** |
| 263 | + * A null body status is a status that is 101, 103, 204, 205, or 304. |
| 264 | + * @see https://fetch.spec.whatwg.org/#null-body-status |
| 265 | + */ |
| 266 | +function nullBodyStatus(status: number): boolean { |
| 267 | + return status === 101 || status === 103 || status === 204 || status === 205 || |
| 268 | + status === 304; |
| 269 | +} |
0 commit comments