• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}
572