nodejs / nodejs/undici

Normalize request headers and query data before invoking composed interceptors

Open
#4,336 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
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

  1. Create a HeadersMap structure that will transmit headers in a single format inside the core, We need a structure with methods like Headers.
  2. Pre-process opts.headers (and opts.query / opts.searchParams) in the core dispatcher before invoking any composed interceptors.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.