response.js 17 KB

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