Part of the audit remediation umbrella #67. Milestone 5. Severity: HIGH (first item), MEDIUM (rest). Requirements: TRANSPORT-11, TRANSPORT-12, TRANSPORT-30, BODY-13.
Current SDK behavior
- Native header rejections escape
send(). UNDICI_FORBIDDEN_HEADERS
(packages/transport-undici/src/undici-transport.ts:58-62) drops content-length, host,
transfer-encoding only. undici 6.28.0 (lib/core/request.js:397-409) throws
InvalidArgumentError for keep-alive and upgrade, NotSupportedError for expect, and
InvalidArgumentError for connection with a value other than close / keep-alive.
toDispatchError (:221-231) maps these to a bare non-retryable TypeError; nothing is
sent. toUndiciHeaders (:150-160) has no per-header guard, so a model-valid non-token name
such as X Custom (core admits printable non-token bytes, ascii-validation.ts:29-33) is
rejected by undici's isValidHTTPToken and the send fails. fetch-transport.ts:146-152 wraps
each native.append in a try/catch and degrades to a drop; undici has no equivalent. No
test exercises a non-token header name.
fileBody bypasses short-write detection. The file-body branch (:259-273) hands
createReadStream(path, {start, end}) to undici directly. writeTo and its
transferred === count invariant (packages/body-file/src/file-body.ts:75-78) never run.
content-length is dropped, so undici frames the body chunked and the wire cannot detect a
short write either. A 1 MB fileBody truncated to 10 bytes before send(): undici POSTs 10
bytes and resolves 200; fetch raises TransportFailureError. The Phase 8a checklist (:80)
marks BODY-13 done while recording the bypass.
- A SOCKS proxy type throws undici's raw error.
toProxyAgentOptions (:115-122) uses
proxy.type as the URI scheme with no check. Core resolves ALL_PROXY=socks5://host:1080 to
type: 'socks5' (config/proxy.ts:34,372-380). new ProxyAgent({uri: 'socks5://…'}) throws
InvalidArgumentError('Invalid URL protocol: …') out of the public factory, untyped and
undocumented.
Expected behavior
undiciTransport().send() does not throw a native error for any model-valid request. A
header the native layer cannot carry is dropped and logged, the same as in the fetch
transport. A file body that ends short of its declared length fails the send with a
TransportFailureError naming transferred-of-total on both transports. undiciTransport()
rejects an unsupported proxy type with a typed, documented error.
Notes and leads
- Drop set: add
expect, keep-alive, upgrade; drop connection values other than
close / keep-alive. Validate each name against RFC 9110 token in toUndiciHeaders and
degrade to a logged drop. Check FETCH_FORBIDDEN_HEADERS (fetch-transport.ts:45-50) too:
on Node the global fetch is undici-backed and its Headers does not implement forbidden
names, so the same rejections likely surface there as a retryable TransportFailureError.
- File body: route through
writeTo via the pumpBody path the streamed case uses, or wrap
the read stream to count bytes and destroy with TransportFailureError when it ends short.
- Proxy: reject
proxy.type !== 'http' with a typed DexpaceError (or the documented
TypeError), add @throws, and either record "SOCKS resolved by core, supported by neither
transport" in docs/deviations.md or drop socks* from ProxyType before the first bump.
- Every item gets a row in
packages/transport-conformance that both transports run:
Expect: 100-continue, Upgrade: websocket, a non-token name, truncate-after-stat.
- Patch changesets for
@dexpace/transport-undici, @dexpace/transport-fetch, and
@dexpace/core if ProxyType changes.
Part of the audit remediation umbrella #67. Milestone 5. Severity: HIGH (first item), MEDIUM (rest). Requirements: TRANSPORT-11, TRANSPORT-12, TRANSPORT-30, BODY-13.
Current SDK behavior
send().UNDICI_FORBIDDEN_HEADERS(
packages/transport-undici/src/undici-transport.ts:58-62) dropscontent-length,host,transfer-encodingonly. undici 6.28.0 (lib/core/request.js:397-409) throwsInvalidArgumentErrorforkeep-aliveandupgrade,NotSupportedErrorforexpect, andInvalidArgumentErrorforconnectionwith a value other thanclose/keep-alive.toDispatchError(:221-231) maps these to a bare non-retryableTypeError; nothing issent.
toUndiciHeaders(:150-160) has no per-header guard, so a model-valid non-token namesuch as
X Custom(core admits printable non-token bytes,ascii-validation.ts:29-33) isrejected by undici's
isValidHTTPTokenand the send fails.fetch-transport.ts:146-152wrapseach
native.appendin atry/catchand degrades to a drop; undici has no equivalent. Notest exercises a non-token header name.
fileBodybypasses short-write detection. The file-body branch (:259-273) handscreateReadStream(path, {start, end})to undici directly.writeToand itstransferred === countinvariant (packages/body-file/src/file-body.ts:75-78) never run.content-lengthis dropped, so undici frames the body chunked and the wire cannot detect ashort write either. A 1 MB
fileBodytruncated to 10 bytes beforesend(): undici POSTs 10bytes and resolves 200; fetch raises
TransportFailureError. The Phase 8a checklist (:80)marks BODY-13 done while recording the bypass.
toProxyAgentOptions(:115-122) usesproxy.typeas the URI scheme with no check. Core resolvesALL_PROXY=socks5://host:1080totype: 'socks5'(config/proxy.ts:34,372-380).new ProxyAgent({uri: 'socks5://…'})throwsInvalidArgumentError('Invalid URL protocol: …')out of the public factory, untyped andundocumented.
Expected behavior
undiciTransport().send()does not throw a native error for any model-valid request. Aheader the native layer cannot carry is dropped and logged, the same as in the fetch
transport. A file body that ends short of its declared length fails the send with a
TransportFailureErrornaming transferred-of-total on both transports.undiciTransport()rejects an unsupported proxy type with a typed, documented error.
Notes and leads
expect,keep-alive,upgrade; dropconnectionvalues other thanclose/keep-alive. Validate each name against RFC 9110tokenintoUndiciHeadersanddegrade to a logged drop. Check
FETCH_FORBIDDEN_HEADERS(fetch-transport.ts:45-50) too:on Node the global fetch is undici-backed and its
Headersdoes not implement forbiddennames, so the same rejections likely surface there as a retryable
TransportFailureError.writeTovia thepumpBodypath the streamed case uses, or wrapthe read stream to count bytes and destroy with
TransportFailureErrorwhen it ends short.proxy.type !== 'http'with a typedDexpaceError(or the documentedTypeError), add@throws, and either record "SOCKS resolved by core, supported by neithertransport" in
docs/deviations.mdor dropsocks*fromProxyTypebefore the first bump.packages/transport-conformancethat both transports run:Expect: 100-continue,Upgrade: websocket, a non-token name, truncate-after-stat.@dexpace/transport-undici,@dexpace/transport-fetch, and@dexpace/coreifProxyTypechanges.