Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions docs/docs/api/Util.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ and parse raw header lists exactly the way undici does.
```mjs
import { util } from 'undici'

const { parseHeaders, headerNameToString } = util
const { parseHeaders, headerNameToString, normalizeHeaders } = util
```

```cjs
const { util } = require('undici')

const { parseHeaders, headerNameToString } = util
const { parseHeaders, headerNameToString, normalizeHeaders } = util
```

## `parseHeaders(headers[, obj])`
Expand Down Expand Up @@ -78,5 +78,27 @@ console.log(util.headerNameToString(Buffer.from('X-Custom-Header')))
// 'x-custom-header'
```

## `normalizeHeaders(headers)`

* `headers` {Object|Array|Iterable|null|undefined} A supported request headers value.
* Returns: {Object} An object keyed by lowercased header name.

Normalizes request headers for dispatchers and interceptors that need to inspect
them. Flat arrays, arrays of name/value pairs, `Headers` objects, custom
iterables, and plain objects are accepted. Repeated names are collected into an
array. The input is not modified.

```mjs
import { util } from 'undici'

const headers = util.normalizeHeaders([
['X-Trace-Id', 'one'],
['x-trace-id', 'two']
])

console.log(headers)
// { 'x-trace-id': [ 'one', 'two' ] }
```

[`Dispatcher`]: Dispatcher.md#class-dispatcher
[`headerNameToString()`]: #headernametostringvalue
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ module.exports.buildConnector = buildConnector
module.exports.errors = errors
module.exports.util = {
parseHeaders: util.parseHeaders,
headerNameToString: util.headerNameToString
headerNameToString: util.headerNameToString,
normalizeHeaders: util.normalizeHeaders
}

function makeDispatcher (fn) {
Expand Down
89 changes: 89 additions & 0 deletions lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,94 @@ function bufferToLowerCasedHeaderName (value) {
return tree.lookup(value) ?? value.toString('latin1').toLowerCase()
}

function appendHeader (headers, key, value) {
if (value === undefined) {
return
}

const name = headerNameToString(key)
const current = Object.hasOwn(headers, name) ? headers[name] : undefined
const values = Array.isArray(value) ? value : [value]

if (current === undefined) {
const headerValue = Array.isArray(value) ? value.slice() : value
if (name === '__proto__') {
Object.defineProperty(headers, name, {
value: headerValue,
enumerable: true,
configurable: true,
writable: true
})
} else {
headers[name] = headerValue
}
} else if (Array.isArray(current)) {
current.push(...values)
} else {
headers[name] = [current, ...values]
}
}

/**
* @param {import('../../types/dispatcher.d.ts').UndiciHeaders | undefined} input
* @returns {Record<string, number | string | string[]>}
*/
function normalizeHeaders (input) {
const headers = {}

if (input == null) {
return headers
}

if (typeof input !== 'object') {
throw new TypeError('headers must be an object')
}

if (!hasSafeIterator(input)) {
for (const key of Object.keys(input)) {
appendHeader(headers, key, input[key])
}
return headers
}

if (Array.isArray(input) && (input.length === 0 || !Array.isArray(input[0]))) {
if (input.length % 2 !== 0) {
throw new TypeError('headers must be a valid header map')
}
for (let i = 0; i < input.length; i += 2) {
const key = input[i]
const value = input[i + 1]
if (typeof key !== 'string' || (typeof value !== 'string' && !Array.isArray(value))) {
throw new TypeError('headers must be a valid header map')
}
if (Array.isArray(value)) {
const mapped = []
for (let j = 0; j < value.length; j++) {
const entry = value[j]
mapped.push(typeof entry === 'string' ? entry : entry.toString('latin1'))
}
appendHeader(headers, key, mapped)
} else {
appendHeader(headers, key, value)
}
}
return headers
}

for (const entry of input) {
if (!Array.isArray(entry) || entry.length !== 2) {
throw new TypeError('headers must be a valid header map')
}
const [key, value] = entry
if (typeof key !== 'string' || (value !== undefined && typeof value !== 'string' && !Array.isArray(value))) {
throw new TypeError('headers must be a valid header map')
}
appendHeader(headers, key, value)
}

return headers
}

/**
* @param {(Buffer | string)[]} headers
* @param {Record<string, string | string[]>} [obj]
Expand Down Expand Up @@ -1014,6 +1102,7 @@ module.exports = {
isAsyncIterable,
isDestroyed,
headerNameToString,
normalizeHeaders,
bufferToLowerCasedHeaderName,
addListener,
removeAllListeners,
Expand Down
3 changes: 1 addition & 2 deletions lib/interceptor/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const {
assertCacheMethods,
getInterceptorOrigin,
makeCacheKey,
normalizeHeaders,
parseCacheControlHeader,
isInvalidOrWildcardVaryHeader,
parseVaryHeader
Expand Down Expand Up @@ -621,7 +620,7 @@ module.exports = (opts = {}) => {

opts = {
...opts,
headers: normalizeHeaders(opts)
headers: util.normalizeHeaders(opts.headers)
}

const reqCacheControl = opts.headers?.['cache-control']
Expand Down
4 changes: 2 additions & 2 deletions lib/interceptor/deduplicate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const diagnosticsChannel = require('node:diagnostics_channel')
const util = require('../core/util')
const DeduplicationHandler = require('../handler/deduplication-handler')
const { getInterceptorOrigin, normalizeHeaders, makeCacheKey, makeDeduplicationKey } = require('../util/cache.js')
const { getInterceptorOrigin, makeCacheKey, makeDeduplicationKey } = require('../util/cache.js')

const pendingRequestsChannel = diagnosticsChannel.channel('undici:request:pending-requests')

Expand Down Expand Up @@ -66,7 +66,7 @@ module.exports = (opts = {}) => {

opts = {
...opts,
headers: normalizeHeaders(opts)
headers: util.normalizeHeaders(opts.headers)
}

// Skip deduplication if request contains any of the specified headers
Expand Down
92 changes: 0 additions & 92 deletions lib/util/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const {
safeHTTPMethods,
pathHasQueryOrFragment,
hasSafeIterator,
isValidHTTPToken
} = require('../core/util')

Expand Down Expand Up @@ -204,96 +203,6 @@ function makeCacheKey (opts, origin = getRequestOrigin(opts)) {
}
}

function appendHeader (headers, key, val) {
const headerName = key.toLowerCase()
const current = headers[headerName]
const values = Array.isArray(val) ? val : [val]

if (current === undefined) {
headers[headerName] = Array.isArray(val) ? val.slice() : val
} else if (Array.isArray(current)) {
current.push(...values)
} else {
headers[headerName] = [current, ...values]
}
}

/**
* @param {Record<string, string[] | string>}
* @returns {Record<string, string[] | string>}
*/
function normalizeHeaders (opts) {
let headers
if (opts.headers == null) {
headers = {}
} else if (typeof opts.headers === 'object') {
headers = {}

if (hasSafeIterator(opts.headers)) {
if (Array.isArray(opts.headers)) {
// Array format: could be flat alternating [k, v, k, v, ...]
// or array-of-pairs [[k, v], ...]
const first = opts.headers[0]
if (Array.isArray(first)) {
for (const x of opts.headers) {
if (!Array.isArray(x)) {
throw new Error('opts.headers is not a valid header map')
}
const [key, val] = x
if (typeof key !== 'string' || typeof val !== 'string') {
throw new Error('opts.headers is not a valid header map')
}
appendHeader(headers, key, val)
}
} else {
// Flat alternating array [k, v, k, v, ...]
const len = opts.headers.length
if (len % 2 !== 0) {
throw new Error('opts.headers is not a valid header map')
}
for (let i = 0; i < len; i += 2) {
const key = opts.headers[i]
const val = opts.headers[i + 1]
if (typeof key !== 'string' || (typeof val !== 'string' && !Array.isArray(val))) {
throw new Error('opts.headers is not a valid header map')
}
if (typeof val === 'string') {
appendHeader(headers, key, val)
} else {
const mapped = []
for (let j = 0; j < val.length; j++) {
const v = val[j]
mapped.push(typeof v === 'string' ? v : v.toString('latin1'))
}
appendHeader(headers, key, mapped)
}
}
}
} else {
// Non-array iterable (e.g. Map) — use original iteration logic
for (const x of opts.headers) {
if (!Array.isArray(x)) {
throw new Error('opts.headers is not a valid header map')
}
const [key, val] = x
if (typeof key !== 'string' || typeof val !== 'string') {
throw new Error('opts.headers is not a valid header map')
}
appendHeader(headers, key, val)
}
}
} else {
for (const key of Object.keys(opts.headers)) {
appendHeader(headers, key, opts.headers[key])
}
}
} else {
throw new Error('opts.headers is not an object')
}

return headers
}

/**
* @param {any} key
*/
Expand Down Expand Up @@ -743,7 +652,6 @@ module.exports = {
getInterceptorOrigin,
getRequestOrigin,
makeCacheKey,
normalizeHeaders,
assertCacheKey,
assertCacheValue,
parseCacheControlHeader,
Expand Down
Loading
Loading