Repository: sindresorhus/got (ts)

Issue:
Custom agent + redirects
I'm seeing the following error:
```
Uncaught error: Error: Protocol "https:" not supported. Expected "http:" 
         at get (/var/app/current/node_modules/got/index.js:33:18) 
         at ClientRequest.fn.request.res (/var/app/current/node_modules/got/index.js:51:5)
```

I think this is caused by passing in a custom agent (from `agentkeepalive`).
1. HTTP and HTTPS require different agent instances
2. If the initial request is HTTP we pass in the HTTP agent
3. If this request then redirects to an HTTPS url, got will still use the HTTP custom agent

Is there a way that we could pass in both an HTTP and HTTPS agent and let got handle the selection of the proper agent? Or alternatively to intercept the redirect and change the agent that will be used?

Pull request description:
Support an object in the `agent` option
Fixes #365

Patch:
diff --git a/index.js b/index.js
index d1668f0..e7d80a9 100644
--- a/index.js
+++ b/index.js
@@ -68,6 +68,7 @@ function requestAsEventEmitter(opts) {
 	const ee = new EventEmitter();
 	const requestUrl = opts.href || urlLib.resolve(urlLib.format(opts), opts.path);
 	const redirects = [];
+	const agents = typeof opts.agent === 'object' ? opts.agent : null;
 	let retryCount = 0;
 	let redirectUrl;
 	let uploadBodySize;
@@ -81,6 +82,11 @@ function requestAsEventEmitter(opts) {
 
 		let fn = opts.protocol === 'https:' ? https : http;
 
+		if (agents) {
+			const protocolName = opts.protocol === 'https:' ? 'https' : 'http';
+			opts.agent = agents[protocolName] || opts.agent;
+		}
+
 		if (opts.useElectronNet && process.versions.electron) {
 			const electron = require('electron');
 			fn = electron.net || electron.remote.net;
@@ -324,7 +330,7 @@ function asPromise(opts) {
 					}
 
 					if (statusCode !== 304 && (statusCode < 200 || statusCode > limitStatusCode)) {
-						throw new got.HTTPError(statusCode, res.headers, opts);
+						throw new got.HTTPError(statusCode, res.statusMessage, res.headers, opts);
 					}
 
 					resolve(res);
@@ -338,6 +344,7 @@ function asPromise(opts) {
 		ee.on('error', reject);
 		ee.on('uploadProgress', proxy.emit.bind(proxy, 'uploadProgress'));
 		ee.on('downloadProgress', proxy.emit.bind(proxy, 'downloadProgress'));
+		ee.on('redirect', proxy.emit.bind(proxy, 'redirect'));
 	});
 
 	const promise = timeoutFn(cancelable);
@@ -407,7 +414,7 @@ function asStream(opts) {
 		res.pipe(output);
 
 		if (statusCode !== 304 && (statusCode < 200 || statusCode > 299)) {
-			proxy.emit('error', new got.HTTPError(statusCode, res.headers, opts), null, res);
+			proxy.emit('error', new got.HTTPError(statusCode, res.statusMessage, res.headers, opts), null, res);
 			return;
 		}
 
@@ -627,8 +634,12 @@ got.ParseError = class extends StdError {
 };
 
 got.HTTPError = class extends StdError {
-	constructor(statusCode, headers, opts) {
-		const statusMessage = http.STATUS_CODES[statusCode];
+	constructor(statusCode, statusMessage, headers, opts) {
+		if (statusMessage) {
+			statusMessage = statusMessage.replace(/\r?\n/g, ' ').trim();
+		} else {
+			statusMessage = http.STATUS_CODES[statusCode];
+		}
 		super(`Response code ${statusCode} (${statusMessage})`, {}, opts);
 		this.name = 'HTTPError';
 		this.statusCode = statusCode;
diff --git a/package.json b/package.json
index 0644a38..2ef78ca 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
 		"url-to-options": "^1.0.1"
 	},
 	"devDependencies": {
-		"ava": "^0.22.0",
+		"ava": "^0.23.0",
 		"coveralls": "^3.0.0",
 		"form-data": "^2.1.1",
 		"get-port": "^3.0.0",
diff --git a/readme.md b/readme.md
index 31c5d11..b9374bb 100644
--- a/readme.md
+++ b/readme.md
@@ -403,6 +403,21 @@ got('todomvc.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()
+	}
+});
+```
+
 
 ## Cookies
 


Test patch:
diff --git a/test/error.js b/test/error.js
index b9d7ab0..f9a116a 100644
--- a/test/error.js
+++ b/test/error.js
@@ -14,6 +14,17 @@ test.before('setup', async () => {
 		res.end('not');
 	});
 
+	s.on('/default-status-message', (req, res) => {
+		res.statusCode = 400;
+		res.end('body');
+	});
+
+	s.on('/custom-status-message', (req, res) => {
+		res.statusCode = 400;
+		res.statusMessage = 'Something Exploded';
+		res.end('body');
+	});
+
 	await s.listen(s.port);
 });
 
@@ -45,6 +56,18 @@ test('options.body error message', async t => {
 	t.regex(err.message, /options\.body must be a ReadableStream, string, Buffer or plain Object/);
 });
 
+test('default status message', async t => {
+	const err = await t.throws(got(`${s.url}/default-status-message`));
+	t.is(err.statusCode, 400);
+	t.is(err.statusMessage, 'Bad Request');
+});
+
+test('custom status message', async t => {
+	const err = await t.throws(got(`${s.url}/custom-status-message`));
+	t.is(err.statusCode, 400);
+	t.is(err.statusMessage, 'Something Exploded');
+});
+
 test.serial('http.request error', async t => {
 	const stub = sinon.stub(http, 'request').callsFake(() => {
 		throw new TypeError('The header content contains invalid characters');
diff --git a/test/redirects.js b/test/redirects.js
index 5306573..bf05b64 100644
--- a/test/redirects.js
+++ b/test/redirects.js
@@ -1,6 +1,9 @@
+import {Agent as HttpAgent} from 'http';
+import {Agent as HttpsAgent} from 'https';
 import test from 'ava';
 import pem from 'pem';
 import pify from 'pify';
+import sinon from 'sinon';
 import got from '..';
 import {createServer, createSSLServer} from './helpers/server';
 
@@ -35,12 +38,22 @@ test.before('setup', async () => {
 	const cert = keys.certificate;
 
 	https = await createSSLServer({key, cert}); // eslint-disable-line object-property-newline
+	http = await createServer();
+
+	// HTTPS Handlers
 
 	https.on('/', (req, res) => {
 		res.end('https');
 	});
 
-	http = await createServer();
+	https.on('/httpsToHttp', (req, res) => {
+		res.writeHead(302, {
+			location: http.url
+		});
+		res.end();
+	});
+
+	// HTTP Handlers
 
 	http.on('/', (req, res) => {
 		res.end('reached');
@@ -173,6 +186,29 @@ test('redirects from http to https works', async t => {
 	t.truthy((await got(`${http.url}/httpToHttps`, {rejectUnauthorized: false})).body);
 });
 
+test('redirects from https to http works', async t => {
+	t.truthy((await got(`${https.url}/httpsToHttp`, {rejectUnauthorized: false})).body);
+});
+
+test('redirects from https to http works with an agent object', async t => {
+	const httpAgent = new HttpAgent({keepAlive: true});
+	const httpsAgent = new HttpsAgent({keepAlive: true});
+	const httpSpy = sinon.spy(httpAgent, 'addRequest');
+	const httpsSpy = sinon.spy(httpsAgent, 'addRequest');
+	t.truthy((await got(`${https.url}/httpsToHttp`, {
+		rejectUnauthorized: false,
+		agent: {
+			http: httpAgent,
+			https: httpsAgent
+		}
+	})).body);
+	t.true(httpSpy.calledOnce);
+	t.true(httpsSpy.calledOnce);
+	// Make sure to close all open sockets
+	httpAgent.destroy();
+	httpsAgent.destroy();
+});
+
 test('redirects works with lowercase method', async t => {
 	const body = (await got(`${http.url}/relative`, {method: 'head'})).body;
 	t.is(body, '');