Repository: sindresorhus/got (ts)

Issue:
Default headers override custom headers from `url` argument
#### Description 
Here we override custom headers (user input aka `...url`) with a default set of headers (aka `...options`) when `url` argument is a object.

https://github.com/sindresorhus/got/blob/8d2e91171509bc2aec71c4483978cfd421ebcf1d/source/normalize-arguments.js#L100-L105

#### Minimal reproducible example of bug

```js
const got = require('got');

const client = got.extend({
    hooks: {
        beforeRequest: [
            async (options) => {
                console.dir('Hook (beforeRequest)', { colors: true });
                console.dir(options.headers, { colors: true });
            }
        ]
    }
});

(async () => {
    try {
        const res = await client.get({
            protocol: 'https:',
            host: 'google.com',
            path: '/',
            headers: {
                'X-Custom-Header': 'some-value'
            }
        });

        console.dir('Requests headers', { colors: true });
        console.dir(res.req.getHeaders(), { colors: true });
    } catch (error) {
        console.dir(error, { colors: true });
    }
})();
```

Output with some `console.log`:

```js
'Before options merging (url.headers)'
{ 'X-Custom-Header': 'some-value' }

'Before options merging (options.headers)'
{ 'user-agent': 'got/9.2.2 (https://github.com/sindresorhus/got)' }

'After options merging'
{ 'user-agent': 'got/9.2.2 (https://github.com/sindresorhus/got)' }

'Hook (beforeRequest)'
{ 'user-agent': 'got/9.2.2 (https://github.com/sindresorhus/got)',
  'accept-encoding': 'gzip, deflate' }

'Requests headers'
{ 'user-agent': 'got/9.2.2 (https://github.com/sindresorhus/got)',
  'accept-encoding': 'gzip, deflate',
  host: 'www.google.com' }
```

Pull request:
Don't override headers defined in the `url` argument when it's an object
Fixes #632

Patch:
diff --git a/source/normalize-arguments.js b/source/normalize-arguments.js
index 7121f56..b1f4de2 100644
--- a/source/normalize-arguments.js
+++ b/source/normalize-arguments.js
@@ -87,9 +87,9 @@ const preNormalize = (options, defaults) => {
 };
 
 const normalize = (url, options, defaults) => {
-	if (is.plainObject(url) && Reflect.has(url, 'url')) {
-		options = url;
-		url = url.url;
+	if (is.plainObject(url)) {
+		options = {...url, ...options};
+		url = options.url || {};
 		delete options.url;
 	}
 
@@ -122,12 +122,8 @@ const normalize = (url, options, defaults) => {
 		url = urlToOptions(url);
 	}
 
-	options = {
-		path: '',
-		...url,
-		protocol: url.protocol || 'https:', // Override both null/undefined with default protocol
-		...options
-	};
+	// Override both null/undefined with default protocol
+	options = merge({path: ''}, url, {protocol: url.protocol || 'https:'}, options);
 
 	const {baseUrl} = options;
 	Object.defineProperty(options, 'baseUrl', {
diff --git a/source/request-as-event-emitter.js b/source/request-as-event-emitter.js
index d21ebc7..d706367 100644
--- a/source/request-as-event-emitter.js
+++ b/source/request-as-event-emitter.js
@@ -276,7 +276,7 @@ module.exports = (options, input) => {
 			}
 
 			if (is.undefined(options.headers['content-length']) && is.undefined(options.headers['transfer-encoding'])) {
-				if (uploadBodySize > 0 || options.method === 'PUT') {
+				if ((uploadBodySize > 0 || options.method === 'PUT') && !is.null(uploadBodySize)) {
 					options.headers['content-length'] = uploadBodySize;
 				}
 			}


Test patch:
diff --git a/test/headers.js b/test/headers.js
index 9cc7ded..ff3f676 100644
--- a/test/headers.js
+++ b/test/headers.js
@@ -44,6 +44,23 @@ test('do not override accept-encoding', async t => {
 	t.is(headers['accept-encoding'], 'gzip');
 });
 
+test('do not remove user headers from `url` object argument', async t => {
+	const headers = (await got({
+		hostname: s.host,
+		port: s.port,
+		json: true,
+		protocol: 'http:',
+		headers: {
+			'X-Request-Id': 'value'
+		}
+	})).body;
+
+	t.is(headers.accept, 'application/json');
+	t.is(headers['user-agent'], `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`);
+	t.is(headers['accept-encoding'], 'gzip, deflate');
+	t.is(headers['x-request-id'], 'value');
+});
+
 test('do not set accept-encoding header when decompress options is false', async t => {
 	const {body: headers} = await got(s.url, {
 		json: true,
diff --git a/test/stream.js b/test/stream.js
index a64e54f..de57d28 100644
--- a/test/stream.js
+++ b/test/stream.js
@@ -189,3 +189,9 @@ test('destroying got.stream() cancels the request', async t => {
 	stream.destroy();
 	t.truthy(req.aborted);
 });
+
+test('piping to got.stream.put()', async t => {
+	await t.notThrowsAsync(async () => {
+		await getStream(got.stream(s.url).pipe(got.stream.put(`${s.url}/post`)));
+	});
+});