Skip to content

fix: collapse identical repeated content-length in legacy dispatcher bridge#5502

Open
mcollina wants to merge 3 commits into
mainfrom
fix-legacy-bridge-content-length
Open

fix: collapse identical repeated content-length in legacy dispatcher bridge#5502
mcollina wants to merge 3 commits into
mainfrom
fix-legacy-bridge-content-length

Conversation

@mcollina

@mcollina mcollina commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

A bare require('undici') (v8) on Node 22/24 silently breaks Node's bundled fetch for any request that sets its own Content-Length header, failing with TypeError: fetch failedInvalidArgumentError: invalid content-length header.

Fixes #5500

Root cause

  • lib/global.js eagerly claims the legacy Symbol.for('undici.globalDispatcher.1') at import time, so the bundled fetch (undici v6 on Node 22, v7 on Node 24) starts dispatching through Dispatcher1Wrapper into the v8 core.
  • The bundled v6/v7 fetch appends its computed content-length even when the request already carries one, producing an identical comma-repeated value (e.g. "58, 58").
  • fix: reject malformed content-length request headers #5060 both added the strict digit-only content-length validator and changed v8's fetch to skip the append when the header is already present — so v8's own fetch never trips the validator, but the bundled v6/v7 fetch got neither half and is rejected.

This is the same class of bridge-semantics divergence as #4990 (allowH2) and #5345 (rawHeaders), this time on request-header validation. Note the issue reports Node 22, but Node 24 (bundled undici 7.x) is affected as well; Node 26 (bundled v8) is not.

Fix

Dispatcher1Wrapper.dispatch() now collapses an identical comma-repeated content-length value to the single value before forwarding, for both plain-object and flat-array header shapes. RFC 9110 §8.6 explicitly permits treating identical repeated field values as one, so this does not reopen the request-smuggling hardening from #5060 — genuinely conflicting values (e.g. "10, 13") are left untouched and still rejected by the core. opts.headers is cloned rather than mutated, and the fast path (no collapse needed) allocates nothing.

Tests

  • test/node-test/global-dispatcher-version.js: end-to-end regression — require('./index.js'), then a POST through the bundled global fetch with a request-set Content-Length. Fails without the fix on Node 22/24, passes with it.
  • test/dispatcher1-wrapper-content-length.js (new): direct wrapper tests — collapse for object and flat-array headers, conflicting values still rejected with UND_ERR_INVALID_ARG, caller headers not mutated.

Verified the issue's own repro script passes against this branch on Node 22.22.1, 24.14.1, and 26.3.0. Full test:unit suite: 1417 pass, 0 fail.

…bridge

Node's bundled fetch (undici v6/v7) appends its computed content-length
even when the request already carries one, producing an identical
comma-repeated value (e.g. "58, 58"). The v1 core tolerated this via
parseInt, but the current core rejects it since #5060, so a bare
require('undici') on Node 22/24 silently broke bundled-fetch requests
that set their own Content-Length.

Collapse identical repeated values (permitted by RFC 9110 section 8.6)
in Dispatcher1Wrapper before dispatching; genuinely conflicting values
are still rejected.

Fixes #5500
@codecov-commenter

codecov-commenter commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.36364% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.46%. Comparing base (a8d1a95) to head (0bba6d7).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/dispatcher/dispatcher1-wrapper.js 96.36% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5502      +/-   ##
==========================================
+ Coverage   93.44%   93.46%   +0.02%     
==========================================
  Files         110      110              
  Lines       37328    37383      +55     
==========================================
+ Hits        34881    34940      +59     
+ Misses       2447     2443       -4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina mcollina requested review from metcoder95, ronag and trivikr July 4, 2026 14:43

@jeswr jeswr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified independently against the original report's environment (Node 22.23.1, bundled undici
6.27.0, undici 8.6.0 on Linux x64):

  • the #5500 repro (both require orders, request-set Content-Length via the bundled global fetch)
    fails on main and passes on this branch;
  • node --test test/dispatcher1-wrapper-content-length.js: 4/4 pass;
  • confirmed off the live dispatch that the bundled v6 fetch produces the identical comma-repeated
    value this collapses (captured "14, 14"), and that conflicting values still reject with
    UND_ERR_INVALID_ARG, preserving the #5060 hardening.

The RFC 9110 §8.6 identical-repeat scoping and the clone-not-mutate handling both look right, and
restricting the collapse to the legacy bridge keeps v8-native paths untouched. LGTM — this fixes
the in-the-wild breakage (fetch-sparql-endpoint ≤7.1.0 / Comunica / CSS SPARQL backends) exactly
at the divergence point.

One optional note, not blocking: object-form headers with an array value
({'content-length': ['13','13']}) aren't collapsed, but no known legacy consumer emits that
shape and the core rejects it either way, so I think leaving it is correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants