source: node_modules/undici/lib/fetch/response.js

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 17.4 KB
Line 
1'use strict'
2
3const { Headers, HeadersList, fill } = require('./headers')
4const { extractBody, cloneBody, mixinBody } = require('./body')
5const util = require('../core/util')
6const { kEnumerableProperty } = util
7const {
8 isValidReasonPhrase,
9 isCancelled,
10 isAborted,
11 isBlobLike,
12 serializeJavascriptValueToJSONString,
13 isErrorLike,
14 isomorphicEncode
15} = require('./util')
16const {
17 redirectStatusSet,
18 nullBodyStatus,
19 DOMException
20} = require('./constants')
21const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
22const { webidl } = require('./webidl')
23const { FormData } = require('./formdata')
24const { getGlobalOrigin } = require('./global')
25const { URLSerializer } = require('./dataURL')
26const { kHeadersList, kConstruct } = require('../core/symbols')
27const assert = require('assert')
28const { types } = require('util')
29
30const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
31const textEncoder = new TextEncoder('utf-8')
32
33// https://fetch.spec.whatwg.org/#response-class
34class Response {
35 // Creates network error Response.
36 static error () {
37 // TODO
38 const relevantRealm = { settingsObject: {} }
39
40 // The static error() method steps are to return the result of creating a
41 // Response object, given a new network error, "immutable", and this’s
42 // relevant Realm.
43 const responseObject = new Response()
44 responseObject[kState] = makeNetworkError()
45 responseObject[kRealm] = relevantRealm
46 responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList
47 responseObject[kHeaders][kGuard] = 'immutable'
48 responseObject[kHeaders][kRealm] = relevantRealm
49 return responseObject
50 }
51
52 // https://fetch.spec.whatwg.org/#dom-response-json
53 static json (data, init = {}) {
54 webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' })
55
56 if (init !== null) {
57 init = webidl.converters.ResponseInit(init)
58 }
59
60 // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
61 const bytes = textEncoder.encode(
62 serializeJavascriptValueToJSONString(data)
63 )
64
65 // 2. Let body be the result of extracting bytes.
66 const body = extractBody(bytes)
67
68 // 3. Let responseObject be the result of creating a Response object, given a new response,
69 // "response", and this’s relevant Realm.
70 const relevantRealm = { settingsObject: {} }
71 const responseObject = new Response()
72 responseObject[kRealm] = relevantRealm
73 responseObject[kHeaders][kGuard] = 'response'
74 responseObject[kHeaders][kRealm] = relevantRealm
75
76 // 4. Perform initialize a response given responseObject, init, and (body, "application/json").
77 initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
78
79 // 5. Return responseObject.
80 return responseObject
81 }
82
83 // Creates a redirect Response that redirects to url with status status.
84 static redirect (url, status = 302) {
85 const relevantRealm = { settingsObject: {} }
86
87 webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' })
88
89 url = webidl.converters.USVString(url)
90 status = webidl.converters['unsigned short'](status)
91
92 // 1. Let parsedURL be the result of parsing url with current settings
93 // object’s API base URL.
94 // 2. If parsedURL is failure, then throw a TypeError.
95 // TODO: base-URL?
96 let parsedURL
97 try {
98 parsedURL = new URL(url, getGlobalOrigin())
99 } catch (err) {
100 throw Object.assign(new TypeError('Failed to parse URL from ' + url), {
101 cause: err
102 })
103 }
104
105 // 3. If status is not a redirect status, then throw a RangeError.
106 if (!redirectStatusSet.has(status)) {
107 throw new RangeError('Invalid status code ' + status)
108 }
109
110 // 4. Let responseObject be the result of creating a Response object,
111 // given a new response, "immutable", and this’s relevant Realm.
112 const responseObject = new Response()
113 responseObject[kRealm] = relevantRealm
114 responseObject[kHeaders][kGuard] = 'immutable'
115 responseObject[kHeaders][kRealm] = relevantRealm
116
117 // 5. Set responseObject’s response’s status to status.
118 responseObject[kState].status = status
119
120 // 6. Let value be parsedURL, serialized and isomorphic encoded.
121 const value = isomorphicEncode(URLSerializer(parsedURL))
122
123 // 7. Append `Location`/value to responseObject’s response’s header list.
124 responseObject[kState].headersList.append('location', value)
125
126 // 8. Return responseObject.
127 return responseObject
128 }
129
130 // https://fetch.spec.whatwg.org/#dom-response
131 constructor (body = null, init = {}) {
132 if (body !== null) {
133 body = webidl.converters.BodyInit(body)
134 }
135
136 init = webidl.converters.ResponseInit(init)
137
138 // TODO
139 this[kRealm] = { settingsObject: {} }
140
141 // 1. Set this’s response to a new response.
142 this[kState] = makeResponse({})
143
144 // 2. Set this’s headers to a new Headers object with this’s relevant
145 // Realm, whose header list is this’s response’s header list and guard
146 // is "response".
147 this[kHeaders] = new Headers(kConstruct)
148 this[kHeaders][kGuard] = 'response'
149 this[kHeaders][kHeadersList] = this[kState].headersList
150 this[kHeaders][kRealm] = this[kRealm]
151
152 // 3. Let bodyWithType be null.
153 let bodyWithType = null
154
155 // 4. If body is non-null, then set bodyWithType to the result of extracting body.
156 if (body != null) {
157 const [extractedBody, type] = extractBody(body)
158 bodyWithType = { body: extractedBody, type }
159 }
160
161 // 5. Perform initialize a response given this, init, and bodyWithType.
162 initializeResponse(this, init, bodyWithType)
163 }
164
165 // Returns response’s type, e.g., "cors".
166 get type () {
167 webidl.brandCheck(this, Response)
168
169 // The type getter steps are to return this’s response’s type.
170 return this[kState].type
171 }
172
173 // Returns response’s URL, if it has one; otherwise the empty string.
174 get url () {
175 webidl.brandCheck(this, Response)
176
177 const urlList = this[kState].urlList
178
179 // The url getter steps are to return the empty string if this’s
180 // response’s URL is null; otherwise this’s response’s URL,
181 // serialized with exclude fragment set to true.
182 const url = urlList[urlList.length - 1] ?? null
183
184 if (url === null) {
185 return ''
186 }
187
188 return URLSerializer(url, true)
189 }
190
191 // Returns whether response was obtained through a redirect.
192 get redirected () {
193 webidl.brandCheck(this, Response)
194
195 // The redirected getter steps are to return true if this’s response’s URL
196 // list has more than one item; otherwise false.
197 return this[kState].urlList.length > 1
198 }
199
200 // Returns response’s status.
201 get status () {
202 webidl.brandCheck(this, Response)
203
204 // The status getter steps are to return this’s response’s status.
205 return this[kState].status
206 }
207
208 // Returns whether response’s status is an ok status.
209 get ok () {
210 webidl.brandCheck(this, Response)
211
212 // The ok getter steps are to return true if this’s response’s status is an
213 // ok status; otherwise false.
214 return this[kState].status >= 200 && this[kState].status <= 299
215 }
216
217 // Returns response’s status message.
218 get statusText () {
219 webidl.brandCheck(this, Response)
220
221 // The statusText getter steps are to return this’s response’s status
222 // message.
223 return this[kState].statusText
224 }
225
226 // Returns response’s headers as Headers.
227 get headers () {
228 webidl.brandCheck(this, Response)
229
230 // The headers getter steps are to return this’s headers.
231 return this[kHeaders]
232 }
233
234 get body () {
235 webidl.brandCheck(this, Response)
236
237 return this[kState].body ? this[kState].body.stream : null
238 }
239
240 get bodyUsed () {
241 webidl.brandCheck(this, Response)
242
243 return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
244 }
245
246 // Returns a clone of response.
247 clone () {
248 webidl.brandCheck(this, Response)
249
250 // 1. If this is unusable, then throw a TypeError.
251 if (this.bodyUsed || (this.body && this.body.locked)) {
252 throw webidl.errors.exception({
253 header: 'Response.clone',
254 message: 'Body has already been consumed.'
255 })
256 }
257
258 // 2. Let clonedResponse be the result of cloning this’s response.
259 const clonedResponse = cloneResponse(this[kState])
260
261 // 3. Return the result of creating a Response object, given
262 // clonedResponse, this’s headers’s guard, and this’s relevant Realm.
263 const clonedResponseObject = new Response()
264 clonedResponseObject[kState] = clonedResponse
265 clonedResponseObject[kRealm] = this[kRealm]
266 clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList
267 clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard]
268 clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm]
269
270 return clonedResponseObject
271 }
272}
273
274mixinBody(Response)
275
276Object.defineProperties(Response.prototype, {
277 type: kEnumerableProperty,
278 url: kEnumerableProperty,
279 status: kEnumerableProperty,
280 ok: kEnumerableProperty,
281 redirected: kEnumerableProperty,
282 statusText: kEnumerableProperty,
283 headers: kEnumerableProperty,
284 clone: kEnumerableProperty,
285 body: kEnumerableProperty,
286 bodyUsed: kEnumerableProperty,
287 [Symbol.toStringTag]: {
288 value: 'Response',
289 configurable: true
290 }
291})
292
293Object.defineProperties(Response, {
294 json: kEnumerableProperty,
295 redirect: kEnumerableProperty,
296 error: kEnumerableProperty
297})
298
299// https://fetch.spec.whatwg.org/#concept-response-clone
300function cloneResponse (response) {
301 // To clone a response response, run these steps:
302
303 // 1. If response is a filtered response, then return a new identical
304 // filtered response whose internal response is a clone of response’s
305 // internal response.
306 if (response.internalResponse) {
307 return filterResponse(
308 cloneResponse(response.internalResponse),
309 response.type
310 )
311 }
312
313 // 2. Let newResponse be a copy of response, except for its body.
314 const newResponse = makeResponse({ ...response, body: null })
315
316 // 3. If response’s body is non-null, then set newResponse’s body to the
317 // result of cloning response’s body.
318 if (response.body != null) {
319 newResponse.body = cloneBody(response.body)
320 }
321
322 // 4. Return newResponse.
323 return newResponse
324}
325
326function makeResponse (init) {
327 return {
328 aborted: false,
329 rangeRequested: false,
330 timingAllowPassed: false,
331 requestIncludesCredentials: false,
332 type: 'default',
333 status: 200,
334 timingInfo: null,
335 cacheState: '',
336 statusText: '',
337 ...init,
338 headersList: init.headersList
339 ? new HeadersList(init.headersList)
340 : new HeadersList(),
341 urlList: init.urlList ? [...init.urlList] : []
342 }
343}
344
345function makeNetworkError (reason) {
346 const isError = isErrorLike(reason)
347 return makeResponse({
348 type: 'error',
349 status: 0,
350 error: isError
351 ? reason
352 : new Error(reason ? String(reason) : reason),
353 aborted: reason && reason.name === 'AbortError'
354 })
355}
356
357function makeFilteredResponse (response, state) {
358 state = {
359 internalResponse: response,
360 ...state
361 }
362
363 return new Proxy(response, {
364 get (target, p) {
365 return p in state ? state[p] : target[p]
366 },
367 set (target, p, value) {
368 assert(!(p in state))
369 target[p] = value
370 return true
371 }
372 })
373}
374
375// https://fetch.spec.whatwg.org/#concept-filtered-response
376function filterResponse (response, type) {
377 // Set response to the following filtered response with response as its
378 // internal response, depending on request’s response tainting:
379 if (type === 'basic') {
380 // A basic filtered response is a filtered response whose type is "basic"
381 // and header list excludes any headers in internal response’s header list
382 // whose name is a forbidden response-header name.
383
384 // Note: undici does not implement forbidden response-header names
385 return makeFilteredResponse(response, {
386 type: 'basic',
387 headersList: response.headersList
388 })
389 } else if (type === 'cors') {
390 // A CORS filtered response is a filtered response whose type is "cors"
391 // and header list excludes any headers in internal response’s header
392 // list whose name is not a CORS-safelisted response-header name, given
393 // internal response’s CORS-exposed header-name list.
394
395 // Note: undici does not implement CORS-safelisted response-header names
396 return makeFilteredResponse(response, {
397 type: 'cors',
398 headersList: response.headersList
399 })
400 } else if (type === 'opaque') {
401 // An opaque filtered response is a filtered response whose type is
402 // "opaque", URL list is the empty list, status is 0, status message
403 // is the empty byte sequence, header list is empty, and body is null.
404
405 return makeFilteredResponse(response, {
406 type: 'opaque',
407 urlList: Object.freeze([]),
408 status: 0,
409 statusText: '',
410 body: null
411 })
412 } else if (type === 'opaqueredirect') {
413 // An opaque-redirect filtered response is a filtered response whose type
414 // is "opaqueredirect", status is 0, status message is the empty byte
415 // sequence, header list is empty, and body is null.
416
417 return makeFilteredResponse(response, {
418 type: 'opaqueredirect',
419 status: 0,
420 statusText: '',
421 headersList: [],
422 body: null
423 })
424 } else {
425 assert(false)
426 }
427}
428
429// https://fetch.spec.whatwg.org/#appropriate-network-error
430function makeAppropriateNetworkError (fetchParams, err = null) {
431 // 1. Assert: fetchParams is canceled.
432 assert(isCancelled(fetchParams))
433
434 // 2. Return an aborted network error if fetchParams is aborted;
435 // otherwise return a network error.
436 return isAborted(fetchParams)
437 ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err }))
438 : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err }))
439}
440
441// https://whatpr.org/fetch/1392.html#initialize-a-response
442function initializeResponse (response, init, body) {
443 // 1. If init["status"] is not in the range 200 to 599, inclusive, then
444 // throw a RangeError.
445 if (init.status !== null && (init.status < 200 || init.status > 599)) {
446 throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
447 }
448
449 // 2. If init["statusText"] does not match the reason-phrase token production,
450 // then throw a TypeError.
451 if ('statusText' in init && init.statusText != null) {
452 // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
453 // reason-phrase = *( HTAB / SP / VCHAR / obs-text )
454 if (!isValidReasonPhrase(String(init.statusText))) {
455 throw new TypeError('Invalid statusText')
456 }
457 }
458
459 // 3. Set response’s response’s status to init["status"].
460 if ('status' in init && init.status != null) {
461 response[kState].status = init.status
462 }
463
464 // 4. Set response’s response’s status message to init["statusText"].
465 if ('statusText' in init && init.statusText != null) {
466 response[kState].statusText = init.statusText
467 }
468
469 // 5. If init["headers"] exists, then fill response’s headers with init["headers"].
470 if ('headers' in init && init.headers != null) {
471 fill(response[kHeaders], init.headers)
472 }
473
474 // 6. If body was given, then:
475 if (body) {
476 // 1. If response's status is a null body status, then throw a TypeError.
477 if (nullBodyStatus.includes(response.status)) {
478 throw webidl.errors.exception({
479 header: 'Response constructor',
480 message: 'Invalid response status code ' + response.status
481 })
482 }
483
484 // 2. Set response's body to body's body.
485 response[kState].body = body.body
486
487 // 3. If body's type is non-null and response's header list does not contain
488 // `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
489 if (body.type != null && !response[kState].headersList.contains('Content-Type')) {
490 response[kState].headersList.append('content-type', body.type)
491 }
492 }
493}
494
495webidl.converters.ReadableStream = webidl.interfaceConverter(
496 ReadableStream
497)
498
499webidl.converters.FormData = webidl.interfaceConverter(
500 FormData
501)
502
503webidl.converters.URLSearchParams = webidl.interfaceConverter(
504 URLSearchParams
505)
506
507// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
508webidl.converters.XMLHttpRequestBodyInit = function (V) {
509 if (typeof V === 'string') {
510 return webidl.converters.USVString(V)
511 }
512
513 if (isBlobLike(V)) {
514 return webidl.converters.Blob(V, { strict: false })
515 }
516
517 if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) {
518 return webidl.converters.BufferSource(V)
519 }
520
521 if (util.isFormDataLike(V)) {
522 return webidl.converters.FormData(V, { strict: false })
523 }
524
525 if (V instanceof URLSearchParams) {
526 return webidl.converters.URLSearchParams(V)
527 }
528
529 return webidl.converters.DOMString(V)
530}
531
532// https://fetch.spec.whatwg.org/#bodyinit
533webidl.converters.BodyInit = function (V) {
534 if (V instanceof ReadableStream) {
535 return webidl.converters.ReadableStream(V)
536 }
537
538 // Note: the spec doesn't include async iterables,
539 // this is an undici extension.
540 if (V?.[Symbol.asyncIterator]) {
541 return V
542 }
543
544 return webidl.converters.XMLHttpRequestBodyInit(V)
545}
546
547webidl.converters.ResponseInit = webidl.dictionaryConverter([
548 {
549 key: 'status',
550 converter: webidl.converters['unsigned short'],
551 defaultValue: 200
552 },
553 {
554 key: 'statusText',
555 converter: webidl.converters.ByteString,
556 defaultValue: ''
557 },
558 {
559 key: 'headers',
560 converter: webidl.converters.HeadersInit
561 }
562])
563
564module.exports = {
565 makeNetworkError,
566 makeResponse,
567 makeAppropriateNetworkError,
568 filterResponse,
569 Response,
570 cloneResponse
571}
Note: See TracBrowser for help on using the repository browser.