Skip to content

Commit d83dadd

Browse files
committed
Add copyPipedHeaders option to control automatic header copying from piped streams
Fixes #1863
1 parent 3e2a781 commit d83dadd

File tree

4 files changed

+156
-2
lines changed

4 files changed

+156
-2
lines changed

documentation/2-options.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,57 @@ Therefore this option has no effect when using HTTP/2.
523523
#### **Note:**
524524
> - The [RFC 7231](https://tools.ietf.org/html/rfc7231#section-4.3.1) doesn't specify any particular behavior for the GET method having a payload, therefore it's considered an [**anti-pattern**](https://en.wikipedia.org/wiki/Anti-pattern).
525525
526+
### `copyPipedHeaders`
527+
528+
**Type: `boolean`**\
529+
**Default: `true`**
530+
531+
Automatically copy headers from piped streams.
532+
533+
When piping a request into a Got stream (e.g., `request.pipe(got.stream(url))`), this controls whether headers from the source stream are automatically merged into the Got request headers.
534+
535+
**Note:** Piped headers overwrite any explicitly set headers with the same name. To override this, either set `copyPipedHeaders` to `false` and manually copy safe headers, or use a `beforeRequest` hook to force specific header values after piping.
536+
537+
Useful for proxy scenarios, but you may want to disable this to filter out headers like `Host`, `Connection`, `Authorization`, etc.
538+
539+
**Example: Disable automatic header copying and manually copy only safe headers**
540+
541+
```js
542+
import got from 'got';
543+
import {pipeline} from 'node:stream/promises';
544+
545+
server.get('/proxy', async (request, response) => {
546+
const gotStream = got.stream('https://example.com', {
547+
copyPipedHeaders: false,
548+
headers: {
549+
'user-agent': request.headers['user-agent'],
550+
'accept': request.headers['accept'],
551+
// Explicitly NOT copying host, connection, authorization, etc.
552+
}
553+
});
554+
555+
await pipeline(request, gotStream, response);
556+
});
557+
```
558+
559+
**Example: Override piped headers using beforeRequest hook**
560+
561+
```js
562+
import got from 'got';
563+
564+
const gotStream = got.stream('https://example.com', {
565+
hooks: {
566+
beforeRequest: [
567+
options => {
568+
// Force specific header values after piping
569+
options.headers.host = 'example.com';
570+
delete options.headers.authorization;
571+
}
572+
]
573+
}
574+
});
575+
```
576+
526577
### `timeout`
527578

528579
**Type: `object`**

source/core/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,8 @@ export default class Request extends Duplex implements RequestEvents<Request> {
243243
this._stopRetry = noop;
244244
this._requestId = generateRequestId();
245245

246-
this.on('pipe', (source: any) => {
247-
if (source?.headers) {
246+
this.on('pipe', (source: NodeJS.ReadableStream & {headers?: Record<string, string | string[] | undefined>}) => {
247+
if (this.options.copyPipedHeaders && source?.headers) {
248248
Object.assign(this.options.headers, source.headers);
249249
}
250250
});

source/core/options.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,7 @@ const defaultInternals: Options['_internals'] = {
10251025
password: '',
10261026
http2: false,
10271027
allowGetBody: false,
1028+
copyPipedHeaders: true,
10281029
headers: {
10291030
'user-agent': 'got (https://github.com/sindresorhus/got)',
10301031
},
@@ -2283,6 +2284,65 @@ export default class Options {
22832284
this._internals.allowGetBody = value;
22842285
}
22852286

2287+
/**
2288+
Automatically copy headers from piped streams.
2289+
2290+
When piping a request into a Got stream (e.g., `request.pipe(got.stream(url))`), this controls whether headers from the source stream are automatically merged into the Got request headers.
2291+
2292+
Note: Piped headers overwrite any explicitly set headers with the same name. To override this, either set `copyPipedHeaders` to `false` and manually copy safe headers, or use a `beforeRequest` hook to force specific header values after piping.
2293+
2294+
Useful for proxy scenarios, but you may want to disable this to filter out headers like `Host`, `Connection`, `Authorization`, etc.
2295+
2296+
@default true
2297+
2298+
@example
2299+
```
2300+
import got from 'got';
2301+
import {pipeline} from 'node:stream/promises';
2302+
2303+
// Disable automatic header copying and manually copy only safe headers
2304+
server.get('/proxy', async (request, response) => {
2305+
const gotStream = got.stream('https://example.com', {
2306+
copyPipedHeaders: false,
2307+
headers: {
2308+
'user-agent': request.headers['user-agent'],
2309+
'accept': request.headers['accept'],
2310+
// Explicitly NOT copying host, connection, authorization, etc.
2311+
}
2312+
});
2313+
2314+
await pipeline(request, gotStream, response);
2315+
});
2316+
```
2317+
2318+
@example
2319+
```
2320+
import got from 'got';
2321+
2322+
// Override piped headers using beforeRequest hook
2323+
const gotStream = got.stream('https://example.com', {
2324+
hooks: {
2325+
beforeRequest: [
2326+
options => {
2327+
// Force specific header values after piping
2328+
options.headers.host = 'example.com';
2329+
delete options.headers.authorization;
2330+
}
2331+
]
2332+
}
2333+
});
2334+
```
2335+
*/
2336+
get copyPipedHeaders(): boolean {
2337+
return this._internals.copyPipedHeaders;
2338+
}
2339+
2340+
set copyPipedHeaders(value: boolean) {
2341+
assert.boolean(value);
2342+
2343+
this._internals.copyPipedHeaders = value;
2344+
}
2345+
22862346
/**
22872347
Request headers.
22882348

test/stream.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,49 @@ test('piping server request to Got proxies also headers', withServer, async (t,
247247
t.is(foo, 'bar');
248248
});
249249

250+
test('piping server request to Got does not proxy headers when copyPipedHeaders is false', withServer, async (t, server, got) => {
251+
server.get('/', headersHandler);
252+
server.get('/proxy', async (request, response) => {
253+
await streamPipeline(
254+
request,
255+
got.stream('', {copyPipedHeaders: false}),
256+
response,
257+
);
258+
});
259+
260+
const headers: Record<string, string> = await got('proxy', {
261+
headers: {
262+
foo: 'bar',
263+
},
264+
}).json();
265+
t.is(headers.foo, undefined);
266+
t.truthy(headers['user-agent']); // Got's default user-agent should still be present
267+
});
268+
269+
test('piped headers overwrite explicitly set headers when copyPipedHeaders is true', withServer, async (t, server, got) => {
270+
server.get('/', headersHandler);
271+
server.get('/proxy', async (request, response) => {
272+
await streamPipeline(
273+
request,
274+
got.stream('', {
275+
copyPipedHeaders: true,
276+
headers: {
277+
'x-custom': 'explicit-value',
278+
},
279+
}),
280+
response,
281+
);
282+
});
283+
284+
const headers: Record<string, string> = await got('proxy', {
285+
headers: {
286+
'x-custom': 'piped-value',
287+
},
288+
}).json();
289+
// Piped header should overwrite explicit header
290+
t.is(headers['x-custom'], 'piped-value');
291+
});
292+
250293
test('skips proxying headers after server has sent them already', withServer, async (t, server, got) => {
251294
server.get('/', defaultHandler);
252295
server.get('/proxy', async (_request, response) => {

0 commit comments

Comments
 (0)