Spaces:
Runtime error
Runtime error
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 { | |
redirectStatusSet, | |
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 | |
const textEncoder = new TextEncoder('utf-8') | |
// 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, 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 = textEncoder.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 (!redirectStatusSet.has(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, err = null) { | |
// 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(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err })) | |
: makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err })) | |
} | |
// 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 | |
} | |