diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 883d7637cbf..a905baef499 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -114,7 +114,7 @@ function appendHeader (headers, name, value) { // forbidden response-header name, return. // 7. Append (name, value) to headers’s header list. - return headers[kHeadersList].append(name, value) + return headers[kHeadersList].append(name, value, false) // 8. If headers’s guard is "request-no-cors", then remove // privileged no-CORS request headers from headers @@ -138,20 +138,14 @@ class HeadersList { /** * @see https://fetch.spec.whatwg.org/#header-list-contains * @param {string} name + * @param {boolean} isLowerCase */ - contains (name) { + contains (name, isLowerCase) { // A header list list contains a header name name if list // contains a header whose name is a byte-case-insensitive // match for name. - return this[kHeadersMap].has(name.toLowerCase()) - } - - /** - * @param {string} name - */ - lowerCaseContains (name) { - return this[kHeadersMap].has(name) + return this[kHeadersMap].has(isLowerCase ? name : name.toLowerCase()) } clear () { @@ -164,13 +158,14 @@ class HeadersList { * @see https://fetch.spec.whatwg.org/#concept-header-list-append * @param {string} name * @param {string} value + * @param {boolean} isLowerCase */ - append (name, value) { + append (name, value, isLowerCase) { this[kHeadersSortedMap] = null // 1. If list contains name, then set name to the first such // header’s name. - const lowercaseName = name.toLowerCase() + const lowercaseName = isLowerCase ? name : name.toLowerCase() const exists = this[kHeadersMap].get(lowercaseName) // 2. Append (name, value) to list. @@ -189,41 +184,15 @@ class HeadersList { } } - /** - * @param {string} name - * @param {string} value - */ - lowerCaseAppend (name, value) { - this[kHeadersSortedMap] = null - - // 1. If list contains name, then set name to the first such - // header’s name. - const exists = this[kHeadersMap].get(name) - - // 2. Append (name, value) to list. - if (exists) { - const delimiter = name === 'cookie' ? '; ' : ', ' - this[kHeadersMap].set(name, { - name: exists.name, - value: `${exists.value}${delimiter}${value}` - }) - } else { - this[kHeadersMap].set(name, { name, value }) - } - - if (name === 'set-cookie') { - (this.cookies ??= []).push(value) - } - } - /** * @see https://fetch.spec.whatwg.org/#concept-header-list-set * @param {string} name * @param {string} value + * @param {boolean} isLowerCase */ - set (name, value) { + set (name, value, isLowerCase) { this[kHeadersSortedMap] = null - const lowercaseName = name.toLowerCase() + const lowercaseName = isLowerCase ? name : name.toLowerCase() if (lowercaseName === 'set-cookie') { this.cookies = [value] @@ -236,35 +205,14 @@ class HeadersList { this[kHeadersMap].set(lowercaseName, { name, value }) } - /** - * @param {string} name - * @param {string} value - */ - lowerCaseSet (name, value) { - if (name === 'set-cookie') { - this.cookies = [value] - } - - // 1. If list contains name, then set the value of - // the first such header to value and remove the - // others. - // 2. Otherwise, append header (name, value) to list. - this[kHeadersMap].set(name, { name, value }) - } - /** * @see https://fetch.spec.whatwg.org/#concept-header-list-delete * @param {string} name + * @param {boolean} isLowerCase */ - delete (name) { - return this.lowerCaseDelete(name.toLowerCase()) - } - - /** - * @param {string} name - */ - lowerCaseDelete (name) { + delete (name, isLowerCase) { this[kHeadersSortedMap] = null + if (!isLowerCase) name = name.toLowerCase() if (name === 'set-cookie') { this.cookies = null @@ -276,24 +224,15 @@ class HeadersList { /** * @see https://fetch.spec.whatwg.org/#concept-header-list-get * @param {string} name + * @param {boolean} isLowerCase * @returns {string | null} */ - get (name) { - const value = this[kHeadersMap].get(name.toLowerCase()) - + get (name, isLowerCase) { // 1. If list does not contain name, then return null. // 2. Return the values of all headers in list whose name // is a byte-case-insensitive match for name, // separated from each other by 0x2C 0x20, in order. - return value === undefined ? null : value.value - } - - /** - * @param {string} name - * @returns {string | null} - */ - lowerCaseGet (name) { - return this[kHeadersMap].get(name)?.value ?? null + return this[kHeadersMap].get(isLowerCase ? name : name.toLowerCase())?.value ?? null } * [Symbol.iterator] () { @@ -383,14 +322,14 @@ class Headers { // 6. If this’s header list does not contain name, then // return. - if (!this[kHeadersList].contains(name)) { + if (!this[kHeadersList].contains(name, false)) { return } // 7. Delete name from this’s header list. // 8. If this’s guard is "request-no-cors", then remove // privileged no-CORS request headers from this. - this[kHeadersList].delete(name) + this[kHeadersList].delete(name, false) } // https://fetch.spec.whatwg.org/#dom-headers-get @@ -412,7 +351,7 @@ class Headers { // 2. Return the result of getting name from this’s header // list. - return this[kHeadersList].get(name) + return this[kHeadersList].get(name, false) } // https://fetch.spec.whatwg.org/#dom-headers-has @@ -434,7 +373,7 @@ class Headers { // 2. Return true if this’s header list contains name; // otherwise false. - return this[kHeadersList].contains(name) + return this[kHeadersList].contains(name, false) } // https://fetch.spec.whatwg.org/#dom-headers-set @@ -483,7 +422,7 @@ class Headers { // 7. Set (name, value) in this’s header list. // 8. If this’s guard is "request-no-cors", then remove // privileged no-CORS request headers from this - this[kHeadersList].set(name, value) + this[kHeadersList].set(name, value, false) } // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 0e5fc35a13d..4a25542c310 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -475,7 +475,7 @@ function fetching ({ } // 12. If request’s header list does not contain `Accept`, then: - if (!request.headersList.lowerCaseContains('accept')) { + if (!request.headersList.contains('accept', true)) { // 1. Let value be `*/*`. const value = '*/*' @@ -492,14 +492,14 @@ function fetching ({ // TODO // 3. Append `Accept`/value to request’s header list. - request.headersList.lowerCaseAppend('accept', value) + request.headersList.append('accept', value, true) } // 13. If request’s header list does not contain `Accept-Language`, then // user agents should append `Accept-Language`/an appropriate value to // request’s header list. - if (!request.headersList.lowerCaseContains('accept-language')) { - request.headersList.lowerCaseAppend('accept-language', '*') + if (!request.headersList.contains('accept-language', true)) { + request.headersList.append('accept-language', '*', true) } // 14. If request’s priority is null, then use request’s initiator and @@ -718,7 +718,7 @@ async function mainFetch (fetchParams, recursive = false) { response.type === 'opaque' && internalResponse.status === 206 && internalResponse.rangeRequested && - !request.headers.lowerCaseContains('range') + !request.headers.contains('range', true) ) { response = internalResponse = makeNetworkError() } @@ -840,7 +840,7 @@ function schemeFetch (fetchParams) { // 8. If request’s header list does not contain `Range`: // 9. Otherwise: - if (!request.headersList.lowerCaseContains('range')) { + if (!request.headersList.contains('range', true)) { // 1. Let bodyWithType be the result of safely extracting blob. // Note: in the FileAPI a blob "object" is a Blob *or* a MediaSource. // In node, this can only ever be a Blob. Therefore we can safely @@ -854,14 +854,14 @@ function schemeFetch (fetchParams) { response.body = bodyWithType[0] // 4. Set response’s header list to « (`Content-Length`, serializedFullLength), (`Content-Type`, type) ». - response.headersList.lowerCaseSet('content-length', serializedFullLength) - response.headersList.lowerCaseSet('content-type', type) + response.headersList.set('content-length', serializedFullLength, true) + response.headersList.set('content-type', type, true) } else { // 1. Set response’s range-requested flag. response.rangeRequested = true // 2. Let rangeHeader be the result of getting `Range` from request’s header list. - const rangeHeader = request.headersList.lowerCaseGet('range') + const rangeHeader = request.headersList.get('range', true) // 3. Let rangeValue be the result of parsing a single range header value given rangeHeader and true. const rangeValue = simpleRangeHeaderValue(rangeHeader, true) @@ -921,9 +921,9 @@ function schemeFetch (fetchParams) { // 15. Set response’s header list to « (`Content-Length`, serializedSlicedLength), // (`Content-Type`, type), (`Content-Range`, contentRange) ». - response.headersList.lowerCaseSet('content-length', serializedSlicedLength) - response.headersList.lowerCaseSet('content-type', type) - response.headersList.lowerCaseSet('content-range', contentRange) + response.headersList.set('content-length', serializedSlicedLength, true) + response.headersList.set('content-type', type, true) + response.headersList.set('content-range', contentRange, true) } // 10. Return response. @@ -1040,7 +1040,7 @@ function fetchFinale (fetchParams, response) { responseStatus = response.status // 2. Let mimeType be the result of extracting a MIME type from response’s header list. - const mimeType = parseMIMEType(response.headersList.lowerCaseGet('content-type')) // TODO: fix + const mimeType = parseMIMEType(response.headersList.get('content-type', true)) // TODO: fix // 3. If mimeType is not failure, then set bodyInfo’s content type to the result of minimizing a supported MIME type given mimeType. if (mimeType !== 'failure') { @@ -1336,11 +1336,11 @@ function httpRedirectFetch (fetchParams, response) { // delete headerName from request’s header list. if (!sameOrigin(requestCurrentURL(request), locationURL)) { // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name - request.headersList.lowerCaseDelete('authorization') + request.headersList.delete('authorization', true) // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement. - request.headersList.lowerCaseDelete('cookie') - request.headersList.lowerCaseDelete('host') + request.headersList.delete('cookie', true) + request.headersList.delete('host', true) } // 14. If request’s body is non-null, then set request’s body to the first return @@ -1456,7 +1456,7 @@ async function httpNetworkOrCacheFetch ( // `Content-Length`/contentLengthHeaderValue to httpRequest’s header // list. if (contentLengthHeaderValue != null) { - httpRequest.headersList.lowerCaseAppend('content-length', contentLengthHeaderValue) + httpRequest.headersList.append('content-length', contentLengthHeaderValue, true) } // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`, @@ -1472,7 +1472,7 @@ async function httpNetworkOrCacheFetch ( // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded, // to httpRequest’s header list. if (httpRequest.referrer instanceof URL) { - httpRequest.headersList.lowerCaseAppend('referer', isomorphicEncode(httpRequest.referrer.href)) + httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href), true) } // 12. Append a request `Origin` header for httpRequest. @@ -1484,8 +1484,8 @@ async function httpNetworkOrCacheFetch ( // 14. If httpRequest’s header list does not contain `User-Agent`, then // user agents should append `User-Agent`/default `User-Agent` value to // httpRequest’s header list. - if (!httpRequest.headersList.lowerCaseContains('user-agent')) { - httpRequest.headersList.lowerCaseAppend('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node') + if (!httpRequest.headersList.contains('user-agent', true)) { + httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node', true) } // 15. If httpRequest’s cache mode is "default" and httpRequest’s header @@ -1494,11 +1494,11 @@ async function httpNetworkOrCacheFetch ( // httpRequest’s cache mode to "no-store". if ( httpRequest.cache === 'default' && - (httpRequest.headersList.lowerCaseContains('if-modified-since') || - httpRequest.headersList.lowerCaseContains('if-none-match') || - httpRequest.headersList.lowerCaseContains('if-unmodified-since') || - httpRequest.headersList.lowerCaseContains('if-match') || - httpRequest.headersList.lowerCaseContains('if-range')) + (httpRequest.headersList.contains('if-modified-since', true) || + httpRequest.headersList.contains('if-none-match', true) || + httpRequest.headersList.contains('if-unmodified-since', true) || + httpRequest.headersList.contains('if-match', true) || + httpRequest.headersList.contains('if-range', true)) ) { httpRequest.cache = 'no-store' } @@ -1510,44 +1510,44 @@ async function httpNetworkOrCacheFetch ( if ( httpRequest.cache === 'no-cache' && !httpRequest.preventNoCacheCacheControlHeaderModification && - !httpRequest.headersList.lowerCaseContains('cache-control') + !httpRequest.headersList.contains('cache-control', true) ) { - httpRequest.headersList.lowerCaseAppend('cache-control', 'max-age=0') + httpRequest.headersList.append('cache-control', 'max-age=0', true) } // 17. If httpRequest’s cache mode is "no-store" or "reload", then: if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') { // 1. If httpRequest’s header list does not contain `Pragma`, then append // `Pragma`/`no-cache` to httpRequest’s header list. - if (!httpRequest.headersList.lowerCaseContains('pragma')) { - httpRequest.headersList.lowerCaseAppend('pragma', 'no-cache') + if (!httpRequest.headersList.contains('pragma', true)) { + httpRequest.headersList.append('pragma', 'no-cache', true) } // 2. If httpRequest’s header list does not contain `Cache-Control`, // then append `Cache-Control`/`no-cache` to httpRequest’s header list. - if (!httpRequest.headersList.lowerCaseContains('cache-control')) { - httpRequest.headersList.lowerCaseAppend('cache-control', 'no-cache') + if (!httpRequest.headersList.contains('cache-control', true)) { + httpRequest.headersList.append('cache-control', 'no-cache', true) } } // 18. If httpRequest’s header list contains `Range`, then append // `Accept-Encoding`/`identity` to httpRequest’s header list. - if (httpRequest.headersList.lowerCaseContains('range')) { - httpRequest.headersList.lowerCaseAppend('accept-encoding', 'identity') + if (httpRequest.headersList.contains('range', true)) { + httpRequest.headersList.append('accept-encoding', 'identity', true) } // 19. Modify httpRequest’s header list per HTTP. Do not append a given // header if httpRequest’s header list contains that header’s name. // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129 - if (!httpRequest.headersList.lowerCaseContains('accept-encoding')) { + if (!httpRequest.headersList.contains('accept-encoding', true)) { if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) { - httpRequest.headersList.lowerCaseAppend('accept-encoding', 'br, gzip, deflate') + httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate', true) } else { - httpRequest.headersList.lowerCaseAppend('accept-encoding', 'gzip, deflate') + httpRequest.headersList.append('accept-encoding', 'gzip, deflate', true) } } - httpRequest.headersList.lowerCaseDelete('host') + httpRequest.headersList.delete('host', true) // 20. If includeCredentials is true, then: if (includeCredentials) { @@ -1630,7 +1630,7 @@ async function httpNetworkOrCacheFetch ( // 12. If httpRequest’s header list contains `Range`, then set response’s // range-requested flag. - if (httpRequest.headersList.lowerCaseContains('range')) { + if (httpRequest.headersList.contains('range', true)) { response.rangeRequested = true } @@ -2121,15 +2121,15 @@ async function httpNetworkFetch ( // We distinguish between them and iterate accordingly if (Array.isArray(rawHeaders)) { for (let i = 0; i < rawHeaders.length; i += 2) { - headersList.lowerCaseAppend(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1')) + headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true) } - const contentEncoding = headersList.lowerCaseGet('content-encoding') + const contentEncoding = headersList.get('content-encoding', true) if (contentEncoding) { // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 // "All content-coding values are case-insensitive..." codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim()) } - location = headersList.lowerCaseGet('location') + location = headersList.get('location', true) } else { const keys = Object.keys(rawHeaders) for (let i = 0; i < keys.length; ++i) { @@ -2243,7 +2243,7 @@ async function httpNetworkFetch ( const headersList = new HeadersList() for (let i = 0; i < rawHeaders.length; i += 2) { - headersList.lowerCaseAppend(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1')) + headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true) } resolve({ diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 54ef3595416..f8894b692eb 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -126,7 +126,7 @@ class Response { const value = isomorphicEncode(URLSerializer(parsedURL)) // 7. Append `Location`/value to responseObject’s response’s header list. - responseObject[kState].headersList.lowerCaseAppend('location', value) + responseObject[kState].headersList.append('location', value, true) // 8. Return responseObject. return responseObject @@ -496,8 +496,8 @@ function initializeResponse (response, init, body) { // 3. If body's type is non-null and response's header list does not contain // `Content-Type`, then append (`Content-Type`, body's type) to response's header list. - if (body.type != null && !response[kState].headersList.lowerCaseContains('content-type')) { - response[kState].headersList.lowerCaseAppend('content-type', body.type) + if (body.type != null && !response[kState].headersList.contains('content-type', true)) { + response[kState].headersList.append('content-type', body.type, true) } } } diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 13c4b468cc6..2693d05bfcf 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -35,7 +35,7 @@ function responseLocationURL (response, requestFragment) { // 2. Let location be the result of extracting header list values given // `Location` and response’s header list. - let location = response.headersList.lowerCaseGet('location') + let location = response.headersList.get('location', true) // 3. If location is a header value, then set location to the result of // parsing location with response’s URL. @@ -153,7 +153,7 @@ function setRequestReferrerPolicyOnRedirect (request, actualResponse) { // 2. Let policy be the empty string. // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token. // 4. Return policy. - const policyHeader = (headersList.lowerCaseGet('referrer-policy') ?? '').split(',') + const policyHeader = (headersList.get('referrer-policy', true) ?? '').split(',') // Note: As the referrer-policy can contain multiple policies // separated by comma, we need to loop through all of them @@ -212,7 +212,7 @@ function appendFetchMetadata (httpRequest) { header = httpRequest.mode // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list. - httpRequest.headersList.lowerCaseSet('sec-fetch-mode', header) + httpRequest.headersList.set('sec-fetch-mode', header, true) // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header // TODO @@ -229,7 +229,7 @@ function appendRequestOriginHeader (request) { // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list. if (request.responseTainting === 'cors' || request.mode === 'websocket') { if (serializedOrigin) { - request.headersList.lowerCaseAppend('origin', serializedOrigin) + request.headersList.append('origin', serializedOrigin, true) } // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: @@ -260,7 +260,7 @@ function appendRequestOriginHeader (request) { if (serializedOrigin) { // 2. Append (`Origin`, serializedOrigin) to request’s header list. - request.headersList.lowerCaseAppend('origin', serializedOrigin) + request.headersList.append('origin', serializedOrigin, true) } } } diff --git a/test/fetch/headers.js b/test/fetch/headers.js index 48461103d78..fcf6f5f4891 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -741,3 +741,12 @@ tap.test('Headers.prototype.getSetCookie', (t) => { t.end() }) + +tap.test('When the value is updated, update the cache', (t) => { + t.plan(2) + const expected = [['a', 'a'], ['b', 'b'], ['c', 'c']] + const headers = new Headers(expected) + t.same([...headers], expected) + headers.append('d', 'd') + t.same([...headers], [...expected, ['d', 'd']]) +})