Repository: sindresorhus/got
Pull request #583 (2018-08-23 16:34:44)

Add ability to pass a custom request function
This would allow an experimental support of http2 using [`http2-wrapper`](https://github.com/szmarczak/http2-wrapper):

```js
const got = require('got');
const wrapper = require('http2-wrapper');

const h2got = got.extend({fn: wrapper});

(async () => {
	const {body} = await h2got('https://nghttp2.org/httpbin/headers');
	console.log(body);
})();
```

sindresorhus: https://github.com/sindresorhus/got/pull/583#discussion_r212383890
sindresorhus: I think we should be explicit an accept the `request` function, not a library with a request function.

Patch:
diff --git a/readme.md b/readme.md
index 6785acc..4a31846 100644
--- a/readme.md
+++ b/readme.md
@@ -266,6 +266,13 @@ Default: `false`
 
 [Cache adapter instance](#cache-adapters) for storing cached data.
 
+###### request
+
+Type: `Function`<br>
+Default: `http.request` `https.request` *(depending on the protocol)*
+
+Custom request function. The main purpose of this is to [support HTTP2 using a wrapper](#experimental-http2-support).
+
 ###### useElectronNet
 
 Type: `boolean`<br>
@@ -282,6 +289,25 @@ Determines if a `got.HTTPError` is thrown for error responses (non-2xx status co
 
 If this is disabled, requests that encounter an error status code will be resolved with the `response` instead of throwing. This may be useful if you are checking for resource availability and are expecting error responses.
 
+###### agent
+
+Same as the [`agent` option](https://nodejs.org/api/http.html#http_http_request_url_options_callback) for `http.request`, but with an extra feature:
+
+If you require different agents for different protocols, you can pass a map of agents to the `agent` option. This is necessary because a request to one protocol might redirect to another. In such a scenario, Got will switch over to the right protocol agent for you.
+
+```js
+const got = require('got');
+const HttpAgent = require('agentkeepalive');
+const {HttpsAgent} = HttpAgent;
+
+got('sindresorhus.com', {
+	agent: {
+		http: new HttpAgent(),
+		https: new HttpsAgent()
+	}
+});
+```
+
 ###### hooks
 
 Type: `Object<string, Function[]>`<br>
@@ -584,21 +610,6 @@ got('sindresorhus.com', {
 });
 ```
 
-If you require different agents for different protocols, you can pass a map of agents to the `agent` option. This is necessary because a request to one protocol might redirect to another. In such a scenario, `got` will switch over to the right protocol agent for you.
-
-```js
-const got = require('got');
-const HttpAgent = require('agentkeepalive');
-const HttpsAgent = HttpAgent.HttpsAgent;
-
-got('sindresorhus.com', {
-	agent: {
-		http: new HttpAgent(),
-		https: new HttpsAgent()
-	}
-});
-```
-
 Check out [`global-tunnel`](https://github.com/np-maintain/global-tunnel) if you want to configure proxy support for all HTTP/HTTPS traffic in your app.
 
 
@@ -814,12 +825,27 @@ const custom = got.extend({
 
 *Need to merge some instances into a single one? Check out [`got.mergeInstances()`](advanced-creation.md#merging-instances).*
 
+### Experimental HTTP2 support
+
+Got provides an experimental support for HTTP2 using the [`http2-wrapper`](https://github.com/szmarczak/http2-wrapper) module:
+
+```js
+const got = require('got');
+const {request} = require('http2-wrapper');
+
+const h2got = got.extend({request});
+
+(async () => {
+	const {body} = await h2got('https://nghttp2.org/httpbin/headers');
+	console.log(body);
+})();
+```
 
 ## Comparison
 
 |                       |  `got`  | `request` | `node-fetch` | `axios` |
 |-----------------------|:-------:|:---------:|:------------:|:-------:|
-| HTTP/2 support        |    ✖    |     ✖    |       ✖      |    ✖   |
+| HTTP/2 support        |    ❔    |     ✖    |       ✖      |    ✖   |
 | Browser support       |    ✖    |     ✖    |       ✔*     |    ✔   |
 | Electron support      |    ✔    |     ✖    |       ✖      |    ✖   |
 | Promise API           |    ✔    |     ✔    |       ✔      |    ✔   |
@@ -844,7 +870,8 @@ const custom = got.extend({
 | Dependents            | ![][gdp] | ![][rdp] |   ![][ndp]   | ![][adp] |
 | Install size          | ![][gis] | ![][ris] |   ![][nis]   | ![][ais] |
 
-\* It's almost API compatible with the browser `fetch` API.
+\* It's almost API compatible with the browser `fetch` API.<br>
+❔ Experimental support.
 
 <!-- ISSUES OPEN -->
 [gio]: https://img.shields.io/github/issues/sindresorhus/got.svg
diff --git a/source/normalize-arguments.js b/source/normalize-arguments.js
index 554c428..f163566 100644
--- a/source/normalize-arguments.js
+++ b/source/normalize-arguments.js
@@ -109,8 +109,10 @@ module.exports = (url, options, defaults) => {
 
 	const {query} = options;
 	if (!is.empty(query) || query instanceof URLSearchParamsGlobal) {
-		const queryParams = new URLSearchParamsGlobal(query);
-		options.path = `${options.path.split('?')[0]}?${queryParams.toString()}`;
+		if (!is.string(query)) {
+			options.query = (new URLSearchParamsGlobal(query)).toString();
+		}
+		options.path = `${options.path.split('?')[0]}?${options.query}`;
 		delete options.query;
 	}
 
diff --git a/source/request-as-event-emitter.js b/source/request-as-event-emitter.js
index 06deb36..9ad7361 100644
--- a/source/request-as-event-emitter.js
+++ b/source/request-as-event-emitter.js
@@ -32,7 +32,12 @@ module.exports = options => {
 			return;
 		}
 
-		let fn = options.protocol === 'https:' ? https : http;
+		let fn;
+		if (is.function(options.request)) {
+			fn = {request: options.request};
+		} else {
+			fn = options.protocol === 'https:' ? https : http;
+		}
 
 		if (agents) {
 			const protocolName = options.protocol === 'https:' ? 'https' : 'http';
@@ -162,12 +167,10 @@ module.exports = options => {
 		try {
 			uploadBodySize = await getBodySize(options);
 
-			if (
-				uploadBodySize > 0 &&
-				is.undefined(options.headers['content-length']) &&
-				is.undefined(options.headers['transfer-encoding'])
-			) {
-				options.headers['content-length'] = uploadBodySize;
+			if (is.undefined(options.headers['content-length']) && is.undefined(options.headers['transfer-encoding'])) {
+				if (uploadBodySize > 0 || options.method === 'PUT') {
+					options.headers['content-length'] = uploadBodySize;
+				}
 			}
 
 			for (const hook of options.hooks.beforeRequest) {


Test patch:
diff --git a/test/arguments.js b/test/arguments.js
index ff8b2c3..faf96d7 100644
--- a/test/arguments.js
+++ b/test/arguments.js
@@ -9,26 +9,20 @@ let s;
 test.before('setup', async () => {
 	s = await createServer();
 
+	const echoUrl = (request, response) => {
+		response.end(request.url);
+	};
+
 	s.on('/', (request, response) => {
 		response.statusCode = 404;
 		response.end();
 	});
 
-	s.on('/test', (request, response) => {
-		response.end(request.url);
-	});
-
-	s.on('/?test=wow', (request, response) => {
-		response.end(request.url);
-	});
-
-	s.on('/test/foobar', (request, response) => {
-		response.end(request.url);
-	});
-
-	s.on('/?test=it’s+ok', (request, response) => {
-		response.end(request.url);
-	});
+	s.on('/test', echoUrl);
+	s.on('/?test=wow', echoUrl);
+	s.on('/test/foobar', echoUrl);
+	s.on('/?test=it’s+ok', echoUrl);
+	s.on('/?test=http://example.com?foo=bar', echoUrl);
 
 	s.on('/stream', (request, response) => {
 		response.end('ok');
@@ -51,6 +45,12 @@ test('url should be utf-8 encoded', async t => {
 	t.regex(error.message, /Parameter `url` must contain valid UTF-8 character sequences/);
 });
 
+test('string url with query is preserved', async t => {
+	const path = `/?test=http://example.com?foo=bar`;
+	const response = await got(`${s.url}${path}`);
+	t.is(response.body, path);
+});
+
 test('options are optional', async t => {
 	t.is((await got(`${s.url}/test`)).body, '/test');
 });
diff --git a/test/create.js b/test/create.js
index a11aa33..ceef942 100644
--- a/test/create.js
+++ b/test/create.js
@@ -166,3 +166,17 @@ test('defaults are cloned on instance creation', t => {
 	t.not(options.foo, instance.defaults.options.foo);
 	t.not(options.hooks.beforeRequest, instance.defaults.options.hooks.beforeRequest);
 });
+
+test('ability to pass a custom request method', async t => {
+	let called = false;
+
+	const request = (...args) => {
+		called = true;
+		return http.request(...args);
+	};
+
+	const instance = got.extend({request});
+	await instance(s.url);
+
+	t.true(called);
+});
diff --git a/test/headers.js b/test/headers.js
index e29516b..574ed8b 100644
--- a/test/headers.js
+++ b/test/headers.js
@@ -80,7 +80,7 @@ test('transform names to lowercase', async t => {
 	t.is(headers['user-agent'], 'test');
 });
 
-test('zero content-length', async t => {
+test('setting content-length to 0', async t => {
 	const {body} = await got(s.url, {
 		headers: {
 			'content-length': 0
@@ -91,6 +91,14 @@ test('zero content-length', async t => {
 	t.is(headers['content-length'], '0');
 });
 
+test('sets content-length to 0 when requesting PUT with empty body', async t => {
+	const {body} = await got(s.url, {
+		method: 'PUT'
+	});
+	const headers = JSON.parse(body);
+	t.is(headers['content-length'], '0');
+});
+
 test('form-data manual content-type', async t => {
 	const form = new FormData();
 	form.append('a', 'b');
diff --git a/test/redirects.js b/test/redirects.js
index f56fb9c..2bbfbfe 100644
--- a/test/redirects.js
+++ b/test/redirects.js
@@ -89,7 +89,7 @@ test.before('setup', async () => {
 		response.end();
 	});
 
-	http.on('/relativeQuery?bang=', (request, response) => {
+	http.on('/relativeQuery?bang', (request, response) => {
 		response.writeHead(302, {
 			location: '/'
 		});