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