mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-02 21:15:27 -04:00
fix(network): strip sec-fetch-* headers to fix Azure DevOps Artifacts 400 errors (#11602)
* fix(network): strip sec-fetch-* headers to fix Azure DevOps Artifacts 400 errors undici's fetch() automatically adds sec-fetch-* headers (e.g. sec-fetch-mode: cors) per the Fetch spec. Azure DevOps Artifacts interprets these as browser requests and returns HTTP 400 for uncached upstream packages. Since pnpm is a CLI tool, these headers serve no purpose. Adds a stripSecFetchHeaders interceptor applied to all dispatchers (global, proxy, and non-proxy) via undici's compose() API. Fixes #11572 * refactor: fix header types and function placement in stripSecFetchHeaders - Widen header type from Record<string, string> to Record<string, string | string[] | undefined> to match Dispatcher.DispatchOptions - Move stripSecFetchHeaders below its first use, relying on function hoisting per codebase conventions * refactor(network.fetch): handle iterable header form and tidy test `Dispatcher.dispatch` accepts headers as a Map/web-Headers iterable in addition to the flat string[] and plain object forms. The previous object branch routed iterables through Object.entries, which would silently drop every header for Map-like inputs. Detect Symbol.iterator and consume the iterator directly when present. Also drop the underscore prefix on the test's `req` parameter since it is used. --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
6
.changeset/fix-ado-sec-fetch-headers.md
Normal file
6
.changeset/fix-ado-sec-fetch-headers.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/network.fetch": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Strip `sec-fetch-*` headers from outgoing HTTP requests. These headers are automatically added by undici's `fetch()` implementation per the Fetch spec but cause Azure DevOps Artifacts to return HTTP 400 for uncached upstream packages, as ADO interprets them as browser requests [#11572](https://github.com/pnpm/pnpm/issues/11572).
|
||||
@@ -27,7 +27,44 @@ setGlobalDispatcher(new Agent({
|
||||
connect: {
|
||||
autoSelectFamily: true,
|
||||
},
|
||||
}))
|
||||
}).compose(stripSecFetchHeaders))
|
||||
|
||||
// undici's fetch() automatically adds sec-fetch-* headers (e.g. sec-fetch-mode: cors)
|
||||
// per the Fetch spec. Some registries like Azure DevOps Artifacts interpret these as
|
||||
// browser requests and reject them with HTTP 400. Since pnpm is a CLI tool, these
|
||||
// headers serve no purpose and must be stripped.
|
||||
// See https://github.com/pnpm/pnpm/issues/11572
|
||||
function stripSecFetchHeaders (dispatch: Dispatcher['dispatch']): Dispatcher['dispatch'] {
|
||||
return (opts, handler) => {
|
||||
if (opts.headers) {
|
||||
if (Array.isArray(opts.headers)) {
|
||||
// Flat array format: [key1, val1, key2, val2, ...]
|
||||
const filtered: string[] = []
|
||||
for (let i = 0; i < opts.headers.length; i += 2) {
|
||||
if (!opts.headers[i].toLowerCase().startsWith('sec-fetch-')) {
|
||||
filtered.push(opts.headers[i], opts.headers[i + 1])
|
||||
}
|
||||
}
|
||||
opts = { ...opts, headers: filtered }
|
||||
} else if (typeof opts.headers === 'object') {
|
||||
// undici also accepts an iterable of [key, value] pairs (e.g. a Map or
|
||||
// web Headers). Use that iterator when present; otherwise fall back to
|
||||
// Object.entries for plain IncomingHttpHeaders objects.
|
||||
const entries = Symbol.iterator in opts.headers
|
||||
? (opts.headers as Iterable<[string, string | string[] | undefined]>)
|
||||
: Object.entries(opts.headers as Record<string, string | string[] | undefined>)
|
||||
const headers: Record<string, string | string[] | undefined> = {}
|
||||
for (const [key, value] of entries) {
|
||||
if (!key.toLowerCase().startsWith('sec-fetch-')) {
|
||||
headers[key] = value
|
||||
}
|
||||
}
|
||||
opts = { ...opts, headers }
|
||||
}
|
||||
}
|
||||
return dispatch(opts, handler)
|
||||
}
|
||||
}
|
||||
|
||||
const DISPATCHER_CACHE = new LRUCache<string, Dispatcher>({
|
||||
max: 50,
|
||||
@@ -165,6 +202,7 @@ function getProxyDispatcher (parsedUri: URL, opts: DispatcherOptions): Dispatche
|
||||
dispatcher = createHttpProxyDispatcher(proxyUrl, isHttps, opts, { ca, cert, key: certKey })
|
||||
}
|
||||
|
||||
dispatcher = dispatcher.compose(stripSecFetchHeaders)
|
||||
DISPATCHER_CACHE.set(key, dispatcher)
|
||||
return dispatcher
|
||||
}
|
||||
@@ -301,8 +339,9 @@ function getNonProxyDispatcher (parsedUri: URL, opts: DispatcherOptions): Dispat
|
||||
},
|
||||
})
|
||||
|
||||
DISPATCHER_CACHE.set(key, agent)
|
||||
return agent
|
||||
const dispatcher = agent.compose(stripSecFetchHeaders)
|
||||
DISPATCHER_CACHE.set(key, dispatcher)
|
||||
return dispatcher
|
||||
}
|
||||
|
||||
function checkNoProxy (parsedUri: URL, opts: { noProxy?: boolean | string }): boolean {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/// <reference path="../../../__typings__/index.d.ts"/>
|
||||
import fs from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
|
||||
import { expect, test } from '@jest/globals'
|
||||
@@ -309,3 +310,25 @@ test('createDispatchedFetch returns a fetch bound to the given dispatcher option
|
||||
await teardownMockAgent()
|
||||
}
|
||||
})
|
||||
|
||||
test('sec-fetch-* headers are stripped from requests', async () => {
|
||||
const receivedHeaders = await new Promise<http.IncomingHttpHeaders>((resolve, reject) => {
|
||||
const server = http.createServer((req, res) => {
|
||||
resolve(req.headers)
|
||||
res.writeHead(200, { 'content-type': 'application/json' })
|
||||
res.end('{"ok":true}')
|
||||
})
|
||||
server.listen(0, () => {
|
||||
const { port } = server.address() as { port: number }
|
||||
const fetchFromRegistry = createFetchFromRegistry({})
|
||||
fetchFromRegistry(`http://127.0.0.1:${port}/test`).then(
|
||||
(res) => res.text().then(() => server.close()),
|
||||
(err) => {
|
||||
server.close(); reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
const secFetchHeaders = Object.keys(receivedHeaders).filter(h => h.startsWith('sec-fetch-'))
|
||||
expect(secFetchHeaders).toEqual([])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user