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