123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- 'use strict'
- const { Headers, HeadersList, fill } = require('./headers')
- const { extractBody, cloneBody, mixinBody } = require('./body')
- const util = require('../core/util')
- const { kEnumerableProperty } = util
- const {
- isValidReasonPhrase,
- isCancelled,
- isAborted,
- isBlobLike,
- serializeJavascriptValueToJSONString,
- isErrorLike,
- isomorphicEncode
- } = require('./util')
- const {
- redirectStatus,
- nullBodyStatus,
- DOMException
- } = require('./constants')
- const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
- const { webidl } = require('./webidl')
- const { FormData } = require('./formdata')
- const { getGlobalOrigin } = require('./global')
- const { URLSerializer } = require('./dataURL')
- const { kHeadersList } = require('../core/symbols')
- const assert = require('assert')
- const { types } = require('util')
- const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
- // https://fetch.spec.whatwg.org/#response-class
- class Response {
- // Creates network error Response.
- static error () {
- // TODO
- const relevantRealm = { settingsObject: {} }
- // The static error() method steps are to return the result of creating a
- // Response object, given a new network error, "immutable", and this’s
- // relevant Realm.
- const responseObject = new Response()
- responseObject[kState] = makeNetworkError()
- responseObject[kRealm] = relevantRealm
- responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList
- responseObject[kHeaders][kGuard] = 'immutable'
- responseObject[kHeaders][kRealm] = relevantRealm
- return responseObject
- }
- // https://fetch.spec.whatwg.org/#dom-response-json
- static json (data = undefined, init = {}) {
- webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' })
- if (init !== null) {
- init = webidl.converters.ResponseInit(init)
- }
- // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
- const bytes = new TextEncoder('utf-8').encode(
- serializeJavascriptValueToJSONString(data)
- )
- // 2. Let body be the result of extracting bytes.
- const body = extractBody(bytes)
- // 3. Let responseObject be the result of creating a Response object, given a new response,
- // "response", and this’s relevant Realm.
- const relevantRealm = { settingsObject: {} }
- const responseObject = new Response()
- responseObject[kRealm] = relevantRealm
- responseObject[kHeaders][kGuard] = 'response'
- responseObject[kHeaders][kRealm] = relevantRealm
- // 4. Perform initialize a response given responseObject, init, and (body, "application/json").
- initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
- // 5. Return responseObject.
- return responseObject
- }
- // Creates a redirect Response that redirects to url with status status.
- static redirect (url, status = 302) {
- const relevantRealm = { settingsObject: {} }
- webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' })
- url = webidl.converters.USVString(url)
- status = webidl.converters['unsigned short'](status)
- // 1. Let parsedURL be the result of parsing url with current settings
- // object’s API base URL.
- // 2. If parsedURL is failure, then throw a TypeError.
- // TODO: base-URL?
- let parsedURL
- try {
- parsedURL = new URL(url, getGlobalOrigin())
- } catch (err) {
- throw Object.assign(new TypeError('Failed to parse URL from ' + url), {
- cause: err
- })
- }
- // 3. If status is not a redirect status, then throw a RangeError.
- if (!redirectStatus.includes(status)) {
- throw new RangeError('Invalid status code ' + status)
- }
- // 4. Let responseObject be the result of creating a Response object,
- // given a new response, "immutable", and this’s relevant Realm.
- const responseObject = new Response()
- responseObject[kRealm] = relevantRealm
- responseObject[kHeaders][kGuard] = 'immutable'
- responseObject[kHeaders][kRealm] = relevantRealm
- // 5. Set responseObject’s response’s status to status.
- responseObject[kState].status = status
- // 6. Let value be parsedURL, serialized and isomorphic encoded.
- const value = isomorphicEncode(URLSerializer(parsedURL))
- // 7. Append `Location`/value to responseObject’s response’s header list.
- responseObject[kState].headersList.append('location', value)
- // 8. Return responseObject.
- return responseObject
- }
- // https://fetch.spec.whatwg.org/#dom-response
- constructor (body = null, init = {}) {
- if (body !== null) {
- body = webidl.converters.BodyInit(body)
- }
- init = webidl.converters.ResponseInit(init)
- // TODO
- this[kRealm] = { settingsObject: {} }
- // 1. Set this’s response to a new response.
- this[kState] = makeResponse({})
- // 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".
- this[kHeaders] = new Headers()
- this[kHeaders][kGuard] = 'response'
- this[kHeaders][kHeadersList] = this[kState].headersList
- this[kHeaders][kRealm] = this[kRealm]
- // 3. Let bodyWithType be null.
- let bodyWithType = null
- // 4. If body is non-null, then set bodyWithType to the result of extracting body.
- if (body != null) {
- const [extractedBody, type] = extractBody(body)
- bodyWithType = { body: extractedBody, type }
- }
- // 5. Perform initialize a response given this, init, and bodyWithType.
- initializeResponse(this, init, bodyWithType)
- }
- // Returns response’s type, e.g., "cors".
- get type () {
- webidl.brandCheck(this, Response)
- // The type getter steps are to return this’s response’s type.
- return this[kState].type
- }
- // Returns response’s URL, if it has one; otherwise the empty string.
- get url () {
- webidl.brandCheck(this, Response)
- const urlList = this[kState].urlList
- // The url getter steps are to return the empty string if this’s
- // response’s URL is null; otherwise this’s response’s URL,
- // serialized with exclude fragment set to true.
- const url = urlList[urlList.length - 1] ?? null
- if (url === null) {
- return ''
- }
- return URLSerializer(url, true)
- }
- // Returns whether response was obtained through a redirect.
- get redirected () {
- webidl.brandCheck(this, Response)
- // The redirected getter steps are to return true if this’s response’s URL
- // list has more than one item; otherwise false.
- return this[kState].urlList.length > 1
- }
- // Returns response’s status.
- get status () {
- webidl.brandCheck(this, Response)
- // The status getter steps are to return this’s response’s status.
- return this[kState].status
- }
- // Returns whether response’s status is an ok status.
- get ok () {
- webidl.brandCheck(this, Response)
- // The ok getter steps are to return true if this’s response’s status is an
- // ok status; otherwise false.
- return this[kState].status >= 200 && this[kState].status <= 299
- }
- // Returns response’s status message.
- get statusText () {
- webidl.brandCheck(this, Response)
- // The statusText getter steps are to return this’s response’s status
- // message.
- return this[kState].statusText
- }
- // Returns response’s headers as Headers.
- get headers () {
- webidl.brandCheck(this, Response)
- // The headers getter steps are to return this’s headers.
- return this[kHeaders]
- }
- get body () {
- webidl.brandCheck(this, Response)
- return this[kState].body ? this[kState].body.stream : null
- }
- get bodyUsed () {
- webidl.brandCheck(this, Response)
- return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
- }
- // Returns a clone of response.
- clone () {
- webidl.brandCheck(this, Response)
- // 1. If this is unusable, then throw a TypeError.
- if (this.bodyUsed || (this.body && this.body.locked)) {
- throw webidl.errors.exception({
- header: 'Response.clone',
- message: 'Body has already been consumed.'
- })
- }
- // 2. Let clonedResponse be the result of cloning this’s response.
- const clonedResponse = cloneResponse(this[kState])
- // 3. Return the result of creating a Response object, given
- // clonedResponse, this’s headers’s guard, and this’s relevant Realm.
- const clonedResponseObject = new Response()
- clonedResponseObject[kState] = clonedResponse
- clonedResponseObject[kRealm] = this[kRealm]
- clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList
- clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard]
- clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm]
- return clonedResponseObject
- }
- }
- mixinBody(Response)
- Object.defineProperties(Response.prototype, {
- type: kEnumerableProperty,
- url: kEnumerableProperty,
- status: kEnumerableProperty,
- ok: kEnumerableProperty,
- redirected: kEnumerableProperty,
- statusText: kEnumerableProperty,
- headers: kEnumerableProperty,
- clone: kEnumerableProperty,
- body: kEnumerableProperty,
- bodyUsed: kEnumerableProperty,
- [Symbol.toStringTag]: {
- value: 'Response',
- configurable: true
- }
- })
- Object.defineProperties(Response, {
- json: kEnumerableProperty,
- redirect: kEnumerableProperty,
- error: kEnumerableProperty
- })
- // https://fetch.spec.whatwg.org/#concept-response-clone
- function cloneResponse (response) {
- // To clone a response response, run these steps:
- // 1. If response is a filtered response, then return a new identical
- // filtered response whose internal response is a clone of response’s
- // internal response.
- if (response.internalResponse) {
- return filterResponse(
- cloneResponse(response.internalResponse),
- response.type
- )
- }
- // 2. Let newResponse be a copy of response, except for its body.
- const newResponse = makeResponse({ ...response, body: null })
- // 3. If response’s body is non-null, then set newResponse’s body to the
- // result of cloning response’s body.
- if (response.body != null) {
- newResponse.body = cloneBody(response.body)
- }
- // 4. Return newResponse.
- return newResponse
- }
- function makeResponse (init) {
- return {
- aborted: false,
- rangeRequested: false,
- timingAllowPassed: false,
- requestIncludesCredentials: false,
- type: 'default',
- status: 200,
- timingInfo: null,
- cacheState: '',
- statusText: '',
- ...init,
- headersList: init.headersList
- ? new HeadersList(init.headersList)
- : new HeadersList(),
- urlList: init.urlList ? [...init.urlList] : []
- }
- }
- function makeNetworkError (reason) {
- const isError = isErrorLike(reason)
- return makeResponse({
- type: 'error',
- status: 0,
- error: isError
- ? reason
- : new Error(reason ? String(reason) : reason),
- aborted: reason && reason.name === 'AbortError'
- })
- }
- function makeFilteredResponse (response, state) {
- state = {
- internalResponse: response,
- ...state
- }
- return new Proxy(response, {
- get (target, p) {
- return p in state ? state[p] : target[p]
- },
- set (target, p, value) {
- assert(!(p in state))
- target[p] = value
- return true
- }
- })
- }
- // https://fetch.spec.whatwg.org/#concept-filtered-response
- function filterResponse (response, type) {
- // Set response to the following filtered response with response as its
- // internal response, depending on request’s response tainting:
- if (type === 'basic') {
- // A basic filtered response is a filtered response whose type is "basic"
- // and header list excludes any headers in internal response’s header list
- // whose name is a forbidden response-header name.
- // Note: undici does not implement forbidden response-header names
- return makeFilteredResponse(response, {
- type: 'basic',
- headersList: response.headersList
- })
- } else if (type === 'cors') {
- // A CORS filtered response is a filtered response whose type is "cors"
- // and header list excludes any headers in internal response’s header
- // list whose name is not a CORS-safelisted response-header name, given
- // internal response’s CORS-exposed header-name list.
- // Note: undici does not implement CORS-safelisted response-header names
- return makeFilteredResponse(response, {
- type: 'cors',
- headersList: response.headersList
- })
- } else if (type === 'opaque') {
- // An opaque filtered response is a filtered response whose type is
- // "opaque", URL list is the empty list, status is 0, status message
- // is the empty byte sequence, header list is empty, and body is null.
- return makeFilteredResponse(response, {
- type: 'opaque',
- urlList: Object.freeze([]),
- status: 0,
- statusText: '',
- body: null
- })
- } else if (type === 'opaqueredirect') {
- // An opaque-redirect filtered response is a filtered response whose type
- // is "opaqueredirect", status is 0, status message is the empty byte
- // sequence, header list is empty, and body is null.
- return makeFilteredResponse(response, {
- type: 'opaqueredirect',
- status: 0,
- statusText: '',
- headersList: [],
- body: null
- })
- } else {
- assert(false)
- }
- }
- // https://fetch.spec.whatwg.org/#appropriate-network-error
- function makeAppropriateNetworkError (fetchParams) {
- // 1. Assert: fetchParams is canceled.
- assert(isCancelled(fetchParams))
- // 2. Return an aborted network error if fetchParams is aborted;
- // otherwise return a network error.
- return isAborted(fetchParams)
- ? makeNetworkError(new DOMException('The operation was aborted.', 'AbortError'))
- : makeNetworkError('Request was cancelled.')
- }
- // https://whatpr.org/fetch/1392.html#initialize-a-response
- function initializeResponse (response, init, body) {
- // 1. If init["status"] is not in the range 200 to 599, inclusive, then
- // throw a RangeError.
- if (init.status !== null && (init.status < 200 || init.status > 599)) {
- throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
- }
- // 2. If init["statusText"] does not match the reason-phrase token production,
- // then throw a TypeError.
- if ('statusText' in init && init.statusText != null) {
- // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
- // reason-phrase = *( HTAB / SP / VCHAR / obs-text )
- if (!isValidReasonPhrase(String(init.statusText))) {
- throw new TypeError('Invalid statusText')
- }
- }
- // 3. Set response’s response’s status to init["status"].
- if ('status' in init && init.status != null) {
- response[kState].status = init.status
- }
- // 4. Set response’s response’s status message to init["statusText"].
- if ('statusText' in init && init.statusText != null) {
- response[kState].statusText = init.statusText
- }
- // 5. If init["headers"] exists, then fill response’s headers with init["headers"].
- if ('headers' in init && init.headers != null) {
- fill(response[kHeaders], init.headers)
- }
- // 6. If body was given, then:
- if (body) {
- // 1. If response's status is a null body status, then throw a TypeError.
- if (nullBodyStatus.includes(response.status)) {
- throw webidl.errors.exception({
- header: 'Response constructor',
- message: 'Invalid response status code ' + response.status
- })
- }
- // 2. Set response's body to body's body.
- response[kState].body = body.body
- // 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.
- if (body.type != null && !response[kState].headersList.contains('Content-Type')) {
- response[kState].headersList.append('content-type', body.type)
- }
- }
- }
- webidl.converters.ReadableStream = webidl.interfaceConverter(
- ReadableStream
- )
- webidl.converters.FormData = webidl.interfaceConverter(
- FormData
- )
- webidl.converters.URLSearchParams = webidl.interfaceConverter(
- URLSearchParams
- )
- // https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
- webidl.converters.XMLHttpRequestBodyInit = function (V) {
- if (typeof V === 'string') {
- return webidl.converters.USVString(V)
- }
- if (isBlobLike(V)) {
- return webidl.converters.Blob(V, { strict: false })
- }
- if (
- types.isAnyArrayBuffer(V) ||
- types.isTypedArray(V) ||
- types.isDataView(V)
- ) {
- return webidl.converters.BufferSource(V)
- }
- if (util.isFormDataLike(V)) {
- return webidl.converters.FormData(V, { strict: false })
- }
- if (V instanceof URLSearchParams) {
- return webidl.converters.URLSearchParams(V)
- }
- return webidl.converters.DOMString(V)
- }
- // https://fetch.spec.whatwg.org/#bodyinit
- webidl.converters.BodyInit = function (V) {
- if (V instanceof ReadableStream) {
- return webidl.converters.ReadableStream(V)
- }
- // Note: the spec doesn't include async iterables,
- // this is an undici extension.
- if (V?.[Symbol.asyncIterator]) {
- return V
- }
- return webidl.converters.XMLHttpRequestBodyInit(V)
- }
- webidl.converters.ResponseInit = webidl.dictionaryConverter([
- {
- key: 'status',
- converter: webidl.converters['unsigned short'],
- defaultValue: 200
- },
- {
- key: 'statusText',
- converter: webidl.converters.ByteString,
- defaultValue: ''
- },
- {
- key: 'headers',
- converter: webidl.converters.HeadersInit
- }
- ])
- module.exports = {
- makeNetworkError,
- makeResponse,
- makeAppropriateNetworkError,
- filterResponse,
- Response,
- cloneResponse
- }
|