Normalize request headers and query data before invoking composed interceptors
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 7.7k
- Forks
- 880
- Avg merge
- 2d 16h
- Merged PRs (30d)
- 68
Description
Description
When using Agent().compose(...) to add custom interceptors, the request’s headers and query data arrive in wildly different shapes depending on how you called request() or fetch(). Users can pass:
- Nothing (opts.headers is undefined)
- Array of strings (['k','v','k2','v2'])
- Plain object ({ k: 'v', k2: 'v2' })
- Headers instance
- Custom iterable (e.g. a user‑defined class with [Symbol.iterator] yielding [name, value] pairs)
Because these values aren’t normalized before your composed interceptors run, every interceptor ends up having to defensively check for multiple shapes:
if (!opts.headers) { … }
else if (Array.isArray(opts.headers)) { … }
else if (opts.headers instanceof Headers) { … }
else if (typeof opts.headers[Symbol.iterator] === 'function') { … }
else if (typeof opts.headers === 'object') { … }
This bloats interceptor code and forces duplication of header‑parsing logic in every piece of middleware (auth, compression, retries, logging, etc.).
Reproduction
import { request, fetch, Agent, Headers, Dispatcher, DecoratorHandler } from 'undici';
function createCustomInterceptor(): Dispatcher.DispatcherComposeInterceptor {
return (dispatch) => (opts, handler) => {
console.log(`-> ${opts.method} ${opts.path}, headers`, opts.headers);
class LoggerHandler extends DecoratorHandler {
onResponseStart(controller, status, headers, statusText) {
console.log(`<- ${status} ${statusText}, headers:`, headers);
super.onResponseStart(controller, status, headers, statusText);
}
}
return dispatch(opts, new LoggerHandler(handler));
};
}
const dispatcher = new Agent().compose(createCustomInterceptor());
class CustomHeaders {
raw = [['k','v'], ['k2','v2']];
*entries() { for (const pair of this.raw) yield pair; }
[Symbol.iterator]() { return this.entries(); }
}
// Try all the different ways to pass headers:
await request('https://api.ipify.org/', { dispatcher });
await request('https://api.ipify.org/', { headers: ['k','v','k2','v2'], dispatcher, responseHeaders: 'raw' });
await request('https://api.ipify.org/', { headers: { k: 'v', k2: 'v2' }, dispatcher });
await request('https://api.ipify.org/', { headers: new Headers([['k','v'],['k2','v2']]), dispatcher });
await request('https://api.ipify.org/', { headers: new CustomHeaders(), dispatcher });
await fetch ('https://api.ipify.org/', { headers: new Headers([['k','v'],['k2','v2']]), dispatcher });
await fetch ('https://api.ipify.org/', { headers: new CustomHeaders(), dispatcher });
Observed output:
-> GET /, headers undefined
-> GET /, headers [ 'k', 'v', 'k2', 'v2' ]
-> GET /, headers { k: 'v', k2: 'v2' }
-> GET /, headers Headers { k: 'v', k2: 'v2' }
-> GET /, headers CustomHeaders {}
…
Expected Behavior
All interceptors should receive a normalized version of:
- opts.headers as a consistent map of lower-cased header names to string values
- opts.query/opts.searchParams similarly normalized into a single, predictable shape
Regardless of how the caller passed them in—arrays, objects, Headers, or any iterable—the dispatcher should convert into one uniform form so that interceptors only ever have to deal with, for example, a plain object { [header: string]: string } or a Headers instance or other HeaderMap structures without validated logical.
Impact
Without dispatcher‑level normalization:
- Boilerplate appears in every interceptor to detect and normalize header shapes.
- Code duplication and potential for subtle bugs increase.
- Reusable, composable middleware becomes harder to write and maintain.
Proposed Solution
- Create a HeadersMap structure that will transmit headers in a single format inside the core, We need a structure with methods like Headers.
- Pre-process opts.headers (and opts.query / opts.searchParams) in the core dispatcher before invoking any composed interceptors.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Agent().compose and the request/fetch dispatcher path shown in the reproduction, then trace how opts.headers and opts.query/searchParams reach composed interceptors. Use the listed array, object, Headers, and CustomHeaders calls to verify the current shapes. Done means interceptors receive one normalized, lower-cased header map and predictable query shape for every listed input.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100