response.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. 'use strict'
  2. const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers')
  3. const { extractBody, cloneBody, mixinBody, hasFinalizationRegistry, 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. isBlobLike,
  12. serializeJavascriptValueToJSONString,
  13. isErrorLike,
  14. isomorphicEncode,
  15. environmentSettingsObject: relevantRealm
  16. } = require('./util')
  17. const {
  18. redirectStatusSet,
  19. nullBodyStatus
  20. } = require('./constants')
  21. const { kState, kHeaders } = require('./symbols')
  22. const { webidl } = require('./webidl')
  23. const { FormData } = require('./formdata')
  24. const { URLSerializer } = require('./data-url')
  25. const { kConstruct } = require('../../core/symbols')
  26. const assert = require('node:assert')
  27. const { types } = require('node:util')
  28. const textEncoder = new TextEncoder('utf-8')
  29. // https://fetch.spec.whatwg.org/#response-class
  30. class Response {
  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 = {}) {
  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[kState].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[kState].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 = {}) {
  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[kState] = 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[kHeaders] = new Headers(kConstruct)
  106. setHeadersGuard(this[kHeaders], 'response')
  107. setHeadersList(this[kHeaders], this[kState].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[kState].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[kState].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[kState].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[kState].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[kState].status >= 200 && this[kState].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[kState].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[kHeaders]
  169. }
  170. get body () {
  171. webidl.brandCheck(this, Response)
  172. return this[kState].body ? this[kState].body.stream : null
  173. }
  174. get bodyUsed () {
  175. webidl.brandCheck(this, Response)
  176. return !!this[kState].body && util.isDisturbed(this[kState].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)) {
  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[kState])
  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[kHeaders]))
  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. mixinBody(Response)
  214. Object.defineProperties(Response.prototype, {
  215. type: kEnumerableProperty,
  216. url: kEnumerableProperty,
  217. status: kEnumerableProperty,
  218. ok: kEnumerableProperty,
  219. redirected: kEnumerableProperty,
  220. statusText: kEnumerableProperty,
  221. headers: kEnumerableProperty,
  222. clone: kEnumerableProperty,
  223. body: kEnumerableProperty,
  224. bodyUsed: kEnumerableProperty,
  225. [Symbol.toStringTag]: {
  226. value: 'Response',
  227. configurable: true
  228. }
  229. })
  230. Object.defineProperties(Response, {
  231. json: kEnumerableProperty,
  232. redirect: kEnumerableProperty,
  233. error: kEnumerableProperty
  234. })
  235. // https://fetch.spec.whatwg.org/#concept-response-clone
  236. function cloneResponse (response) {
  237. // To clone a response response, run these steps:
  238. // 1. If response is a filtered response, then return a new identical
  239. // filtered response whose internal response is a clone of response’s
  240. // internal response.
  241. if (response.internalResponse) {
  242. return filterResponse(
  243. cloneResponse(response.internalResponse),
  244. response.type
  245. )
  246. }
  247. // 2. Let newResponse be a copy of response, except for its body.
  248. const newResponse = makeResponse({ ...response, body: null })
  249. // 3. If response’s body is non-null, then set newResponse’s body to the
  250. // result of cloning response’s body.
  251. if (response.body != null) {
  252. newResponse.body = cloneBody(newResponse, response.body)
  253. }
  254. // 4. Return newResponse.
  255. return newResponse
  256. }
  257. function makeResponse (init) {
  258. return {
  259. aborted: false,
  260. rangeRequested: false,
  261. timingAllowPassed: false,
  262. requestIncludesCredentials: false,
  263. type: 'default',
  264. status: 200,
  265. timingInfo: null,
  266. cacheState: '',
  267. statusText: '',
  268. ...init,
  269. headersList: init?.headersList
  270. ? new HeadersList(init?.headersList)
  271. : new HeadersList(),
  272. urlList: init?.urlList ? [...init.urlList] : []
  273. }
  274. }
  275. function makeNetworkError (reason) {
  276. const isError = isErrorLike(reason)
  277. return makeResponse({
  278. type: 'error',
  279. status: 0,
  280. error: isError
  281. ? reason
  282. : new Error(reason ? String(reason) : reason),
  283. aborted: reason && reason.name === 'AbortError'
  284. })
  285. }
  286. // @see https://fetch.spec.whatwg.org/#concept-network-error
  287. function isNetworkError (response) {
  288. return (
  289. // A network error is a response whose type is "error",
  290. response.type === 'error' &&
  291. // status is 0
  292. response.status === 0
  293. )
  294. }
  295. function makeFilteredResponse (response, state) {
  296. state = {
  297. internalResponse: response,
  298. ...state
  299. }
  300. return new Proxy(response, {
  301. get (target, p) {
  302. return p in state ? state[p] : target[p]
  303. },
  304. set (target, p, value) {
  305. assert(!(p in state))
  306. target[p] = value
  307. return true
  308. }
  309. })
  310. }
  311. // https://fetch.spec.whatwg.org/#concept-filtered-response
  312. function filterResponse (response, type) {
  313. // Set response to the following filtered response with response as its
  314. // internal response, depending on request’s response tainting:
  315. if (type === 'basic') {
  316. // A basic filtered response is a filtered response whose type is "basic"
  317. // and header list excludes any headers in internal response’s header list
  318. // whose name is a forbidden response-header name.
  319. // Note: undici does not implement forbidden response-header names
  320. return makeFilteredResponse(response, {
  321. type: 'basic',
  322. headersList: response.headersList
  323. })
  324. } else if (type === 'cors') {
  325. // A CORS filtered response is a filtered response whose type is "cors"
  326. // and header list excludes any headers in internal response’s header
  327. // list whose name is not a CORS-safelisted response-header name, given
  328. // internal response’s CORS-exposed header-name list.
  329. // Note: undici does not implement CORS-safelisted response-header names
  330. return makeFilteredResponse(response, {
  331. type: 'cors',
  332. headersList: response.headersList
  333. })
  334. } else if (type === 'opaque') {
  335. // An opaque filtered response is a filtered response whose type is
  336. // "opaque", URL list is the empty list, status is 0, status message
  337. // is the empty byte sequence, header list is empty, and body is null.
  338. return makeFilteredResponse(response, {
  339. type: 'opaque',
  340. urlList: Object.freeze([]),
  341. status: 0,
  342. statusText: '',
  343. body: null
  344. })
  345. } else if (type === 'opaqueredirect') {
  346. // An opaque-redirect filtered response is a filtered response whose type
  347. // is "opaqueredirect", status is 0, status message is the empty byte
  348. // sequence, header list is empty, and body is null.
  349. return makeFilteredResponse(response, {
  350. type: 'opaqueredirect',
  351. status: 0,
  352. statusText: '',
  353. headersList: [],
  354. body: null
  355. })
  356. } else {
  357. assert(false)
  358. }
  359. }
  360. // https://fetch.spec.whatwg.org/#appropriate-network-error
  361. function makeAppropriateNetworkError (fetchParams, err = null) {
  362. // 1. Assert: fetchParams is canceled.
  363. assert(isCancelled(fetchParams))
  364. // 2. Return an aborted network error if fetchParams is aborted;
  365. // otherwise return a network error.
  366. return isAborted(fetchParams)
  367. ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err }))
  368. : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err }))
  369. }
  370. // https://whatpr.org/fetch/1392.html#initialize-a-response
  371. function initializeResponse (response, init, body) {
  372. // 1. If init["status"] is not in the range 200 to 599, inclusive, then
  373. // throw a RangeError.
  374. if (init.status !== null && (init.status < 200 || init.status > 599)) {
  375. throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
  376. }
  377. // 2. If init["statusText"] does not match the reason-phrase token production,
  378. // then throw a TypeError.
  379. if ('statusText' in init && init.statusText != null) {
  380. // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
  381. // reason-phrase = *( HTAB / SP / VCHAR / obs-text )
  382. if (!isValidReasonPhrase(String(init.statusText))) {
  383. throw new TypeError('Invalid statusText')
  384. }
  385. }
  386. // 3. Set response’s response’s status to init["status"].
  387. if ('status' in init && init.status != null) {
  388. response[kState].status = init.status
  389. }
  390. // 4. Set response’s response’s status message to init["statusText"].
  391. if ('statusText' in init && init.statusText != null) {
  392. response[kState].statusText = init.statusText
  393. }
  394. // 5. If init["headers"] exists, then fill response’s headers with init["headers"].
  395. if ('headers' in init && init.headers != null) {
  396. fill(response[kHeaders], init.headers)
  397. }
  398. // 6. If body was given, then:
  399. if (body) {
  400. // 1. If response's status is a null body status, then throw a TypeError.
  401. if (nullBodyStatus.includes(response.status)) {
  402. throw webidl.errors.exception({
  403. header: 'Response constructor',
  404. message: `Invalid response status code ${response.status}`
  405. })
  406. }
  407. // 2. Set response's body to body's body.
  408. response[kState].body = body.body
  409. // 3. If body's type is non-null and response's header list does not contain
  410. // `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
  411. if (body.type != null && !response[kState].headersList.contains('content-type', true)) {
  412. response[kState].headersList.append('content-type', body.type, true)
  413. }
  414. }
  415. }
  416. /**
  417. * @see https://fetch.spec.whatwg.org/#response-create
  418. * @param {any} innerResponse
  419. * @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard
  420. * @returns {Response}
  421. */
  422. function fromInnerResponse (innerResponse, guard) {
  423. const response = new Response(kConstruct)
  424. response[kState] = innerResponse
  425. response[kHeaders] = new Headers(kConstruct)
  426. setHeadersList(response[kHeaders], innerResponse.headersList)
  427. setHeadersGuard(response[kHeaders], guard)
  428. if (hasFinalizationRegistry && innerResponse.body?.stream) {
  429. // If the target (response) is reclaimed, the cleanup callback may be called at some point with
  430. // the held value provided for it (innerResponse.body.stream). The held value can be any value:
  431. // a primitive or an object, even undefined. If the held value is an object, the registry keeps
  432. // a strong reference to it (so it can pass it to the cleanup callback later). Reworded from
  433. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
  434. streamRegistry.register(response, new WeakRef(innerResponse.body.stream))
  435. }
  436. return response
  437. }
  438. webidl.converters.ReadableStream = webidl.interfaceConverter(
  439. ReadableStream
  440. )
  441. webidl.converters.FormData = webidl.interfaceConverter(
  442. FormData
  443. )
  444. webidl.converters.URLSearchParams = webidl.interfaceConverter(
  445. URLSearchParams
  446. )
  447. // https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
  448. webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) {
  449. if (typeof V === 'string') {
  450. return webidl.converters.USVString(V, prefix, name)
  451. }
  452. if (isBlobLike(V)) {
  453. return webidl.converters.Blob(V, prefix, name, { strict: false })
  454. }
  455. if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) {
  456. return webidl.converters.BufferSource(V, prefix, name)
  457. }
  458. if (util.isFormDataLike(V)) {
  459. return webidl.converters.FormData(V, prefix, name, { strict: false })
  460. }
  461. if (V instanceof URLSearchParams) {
  462. return webidl.converters.URLSearchParams(V, prefix, name)
  463. }
  464. return webidl.converters.DOMString(V, prefix, name)
  465. }
  466. // https://fetch.spec.whatwg.org/#bodyinit
  467. webidl.converters.BodyInit = function (V, prefix, argument) {
  468. if (V instanceof ReadableStream) {
  469. return webidl.converters.ReadableStream(V, prefix, argument)
  470. }
  471. // Note: the spec doesn't include async iterables,
  472. // this is an undici extension.
  473. if (V?.[Symbol.asyncIterator]) {
  474. return V
  475. }
  476. return webidl.converters.XMLHttpRequestBodyInit(V, prefix, argument)
  477. }
  478. webidl.converters.ResponseInit = webidl.dictionaryConverter([
  479. {
  480. key: 'status',
  481. converter: webidl.converters['unsigned short'],
  482. defaultValue: () => 200
  483. },
  484. {
  485. key: 'statusText',
  486. converter: webidl.converters.ByteString,
  487. defaultValue: () => ''
  488. },
  489. {
  490. key: 'headers',
  491. converter: webidl.converters.HeadersInit
  492. }
  493. ])
  494. module.exports = {
  495. isNetworkError,
  496. makeNetworkError,
  497. makeResponse,
  498. makeAppropriateNetworkError,
  499. filterResponse,
  500. Response,
  501. cloneResponse,
  502. fromInnerResponse
  503. }