response.js 19 KB

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