response.js 19 KB

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