Repository: sindresorhus/got (ts)

Issue:
Redirection to a malformed URI crashes whole process
Demo:
```javascript
const got = require('got');

(async function() {
    try {
        var response = await got('http://shorturl.at/dsDT8'); // redirects to https://google.com/%D8
    } catch (e) {
        console.log('caught:', e);
    }

    console.log('it should display this');
})();
```

Result:
```bash
$ node blah
/home/szymon/Desktop/server/node_modules/normalize-url/index.js:85
                urlObj.pathname = decodeURI(urlObj.pathname);
                                  ^

URIError: URI malformed
    at decodeURI (<anonymous>)
    at module.exports (/home/szymon/Desktop/server/node_modules/normalize-url/index.js:85:21)
    at /home/szymon/Desktop/server/node_modules/cacheable-request/src/index.js:43:16
    at get (/home/szymon/Desktop/server/node_modules/got/index.js:98:20)
    at res (/home/szymon/Desktop/server/node_modules/got/index.js:140:5)
    at ClientRequest.handler (/home/szymon/Desktop/server/node_modules/cacheable-request/src/index.js:88:7)
    at Object.onceWrapper (events.js:275:13)
    at ClientRequest.emit (events.js:182:13)
    at HTTPParser.parserOnIncomingClient [as onIncoming] (_http_client.js:534:21)
    at HTTPParser.parserOnHeadersComplete (_http_common.js:109:17)
```

There are two possible solutions:
~1. Support malformed URIs.~
2. Throw an error.

~I would recommend the first solution, because you're able to visit `https://google.com/%D8` in browser, even more: you can make a request to that using `http`/`https` module!~

~Because that's upstream issue, I suggest adding a new option: `throwsOnMalformedURI` which is `true` by default.~

EDIT: the first solution would cause too many problems. It should throw a Got error.

Pull request:
Catch more errors
Fixes #494.

Patch:
diff --git a/index.js b/index.js
index 62cd65e..4c8f417 100644
--- a/index.js
+++ b/index.js
@@ -7,6 +7,7 @@ const {PassThrough, Transform} = require('stream');
 const urlLib = require('url');
 const fs = require('fs');
 const querystring = require('querystring');
+const extend = require('extend');
 const CacheableRequest = require('cacheable-request');
 const duplexer3 = require('duplexer3');
 const toReadableStream = require('to-readable-stream');
@@ -28,6 +29,19 @@ const errors = require('./errors');
 const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
 const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
 
+const defaults = {
+	retries: 2,
+	cache: false,
+	decompress: true,
+	useElectronNet: false,
+	throwHttpErrors: true,
+	headers: {
+		'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
+	}
+};
+
+const got = create(defaults);
+
 const isFormData = body => is.nodeStream(body) && is.function(body.getBoundary);
 
 const getBodySize = async opts => {
@@ -130,6 +144,13 @@ function requestAsEventEmitter(opts) {
 
 				redirectUrl = urlLib.resolve(urlLib.format(opts), bufferString);
 
+				try {
+					decodeURI(redirectUrl);
+				} catch (err) {
+					ee.emit('error', err);
+					return;
+				}
+
 				redirects.push(redirectUrl);
 
 				const redirectOpts = {
@@ -147,8 +168,8 @@ function requestAsEventEmitter(opts) {
 			setImmediate(() => {
 				try {
 					getResponse(res, opts, ee, redirects);
-				} catch (e) {
-					ee.emit('error', e);
+				} catch (err) {
+					ee.emit('error', err);
 				}
 			});
 		});
@@ -177,7 +198,13 @@ function requestAsEventEmitter(opts) {
 				const backoff = opts.retries(++retryCount, err);
 
 				if (backoff) {
-					setTimeout(get, backoff, opts);
+					setTimeout(opts => {
+						try {
+							get(opts);
+						} catch (e) {
+							ee.emit('error', e);
+						}
+					}, backoff, opts);
 					return;
 				}
 
@@ -511,17 +538,8 @@ function normalizeArguments(url, opts) {
 		url = urlToOptions(url);
 	}
 
-	const defaults = {
-		path: '',
-		retries: 2,
-		cache: false,
-		decompress: true,
-		useElectronNet: false,
-		throwHttpErrors: true
-	};
-
 	opts = {
-		...defaults,
+		path: '',
 		...url,
 		protocol: url.protocol || 'http:', // Override both null/undefined with default protocol
 		...opts
@@ -534,10 +552,7 @@ function normalizeArguments(url, opts) {
 		}
 	}
 
-	opts.headers = {
-		'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`,
-		...headers
-	};
+	opts.headers = headers;
 
 	if (opts.decompress && is.undefined(opts.headers['accept-encoding'])) {
 		opts.headers['accept-encoding'] = 'gzip, deflate';
@@ -641,36 +656,46 @@ function normalizeArguments(url, opts) {
 	return opts;
 }
 
-function got(url, opts) {
-	try {
-		const normalizedArgs = normalizeArguments(url, opts);
+function create(defaults = {}) {
+	function got(url, opts) {
+		try {
+			opts = extend(true, {}, defaults, opts);
+			const normalizedArgs = normalizeArguments(url, opts);
 
-		if (normalizedArgs.stream) {
-			return asStream(normalizedArgs);
-		}
+			if (normalizedArgs.stream) {
+				return asStream(normalizedArgs);
+			}
 
-		return asPromise(normalizedArgs);
-	} catch (err) {
-		return Promise.reject(err);
+			return asPromise(normalizedArgs);
+		} catch (err) {
+			return Promise.reject(err);
+		}
 	}
-}
 
-got.stream = (url, opts) => asStream(normalizeArguments(url, opts));
+	got.create = (opts = {}) => create(extend(true, {}, defaults, opts));
 
-const methods = [
-	'get',
-	'post',
-	'put',
-	'patch',
-	'head',
-	'delete'
-];
+	got.stream = (url, opts) => {
+		opts = extend(true, {}, defaults, opts);
+		return asStream(normalizeArguments(url, opts));
+	};
 
-for (const method of methods) {
-	got[method] = (url, options) => got(url, {...options, method});
-	got.stream[method] = (url, options) => got.stream(url, {...options, method});
-}
+	const methods = [
+		'get',
+		'post',
+		'put',
+		'patch',
+		'head',
+		'delete'
+	];
+
+	for (const method of methods) {
+		got[method] = (url, options) => got(url, {...options, method});
+		got.stream[method] = (url, options) => got.stream(url, {...options, method});
+	}
+
+	Object.assign(got, errors);
 
-Object.assign(got, errors);
+	return got;
+}
 
 module.exports = got;
diff --git a/package.json b/package.json
index 88f2aa8..5b2f73a 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
 		"cacheable-request": "^2.1.1",
 		"decompress-response": "^3.3.0",
 		"duplexer3": "^0.1.4",
+		"extend": "^3.0.1",
 		"get-stream": "^3.0.0",
 		"is-retry-allowed": "^1.1.0",
 		"isurl": "^1.0.0-alpha5",
diff --git a/readme.md b/readme.md
index ce9fe85..5401674 100644
--- a/readme.md
+++ b/readme.md
@@ -291,6 +291,25 @@ If it's not possible to retrieve the body size (can happen when streaming), `tot
 
 Sets `options.method` to the method name and makes a request.
 
+#### Instances
+
+#### got.create([options])
+
+Configure a new `got` instance with default `options`:
+
+```js
+(async () => {
+	const client = got.create({headers: {'x-foo': 'bar'}});
+	const {headers} = await client.get('httpbin.org/headers', {json: true}).body;
+	//=> headers['x-foo'] === 'bar'
+
+	const jsonClient = client.create({json: true, headers: {'x-baz': 'qux'}});
+	const {headers: headers2} = await jsonClient.get('httpbin.org/headers').body;
+	//=> headers2['x-foo'] === 'bar'
+	//=> headers2['x-baz'] === 'qux'
+})();
+```
+
 
 ## Errors
 


Test patch:
diff --git a/test/create.js b/test/create.js
new file mode 100644
index 0000000..5bc98be
--- /dev/null
+++ b/test/create.js
@@ -0,0 +1,67 @@
+import test from 'ava';
+import got from '..';
+import {createServer} from './helpers/server';
+
+let s;
+
+test.before('setup', async () => {
+	s = await createServer();
+
+	s.on('/', (req, res) => {
+		req.resume();
+		res.end(JSON.stringify(req.headers));
+	});
+
+	await s.listen(s.port);
+});
+
+test.after('cleanup', async () => {
+	await s.close();
+});
+
+test('preserve global defaults', async t => {
+	const globalHeaders = (await got(s.url, {json: true})).body;
+	const instanceHeaders = (await got.create()(s.url, {json: true})).body;
+	t.deepEqual(instanceHeaders, globalHeaders);
+});
+
+test('support instance defaults', async t => {
+	const instance = got.create({
+		headers: {
+			'user-agent': 'custom-ua-string'
+		}
+	});
+	const headers = (await instance(s.url, {json: true})).body;
+	t.is(headers['user-agent'], 'custom-ua-string');
+});
+
+test('support invocation overrides', async t => {
+	const instance = got.create({
+		headers: {
+			'user-agent': 'custom-ua-string'
+		}
+	});
+	const headers = (await instance(s.url, {
+		json: true,
+		headers: {
+			'user-agent': 'different-ua-string'
+		}
+	})).body;
+	t.is(headers['user-agent'], 'different-ua-string');
+});
+
+test('curry previous instance defaults', async t => {
+	const instanceA = got.create({
+		headers: {
+			'x-foo': 'foo'
+		}
+	});
+	const instanceB = instanceA.create({
+		headers: {
+			'x-bar': 'bar'
+		}
+	});
+	const headers = (await instanceB(s.url, {json: true})).body;
+	t.is(headers['x-foo'], 'foo');
+	t.is(headers['x-bar'], 'bar');
+});
diff --git a/test/redirects.js b/test/redirects.js
index b1ee333..d7a60b9 100644
--- a/test/redirects.js
+++ b/test/redirects.js
@@ -123,6 +123,13 @@ test.before('setup', async () => {
 		res.end();
 	});
 
+	http.on('/malformedRedirect', (req, res) => {
+		res.writeHead(302, {
+			location: '/%D8'
+		});
+		res.end();
+	});
+
 	await http.listen(http.port);
 	await https.listen(https.port);
 });
@@ -210,3 +217,8 @@ test('redirect response contains old url', async t => {
 test('redirect response contains utf8 with binary encoding', async t => {
 	t.is((await got(`${http.url}/redirect-with-utf8-binary`)).body, 'reached');
 });
+
+test('throws on malformed redirect URI', async t => {
+	const err = await t.throws(got(`${http.url}/malformedRedirect`));
+	t.is(err.name, 'URIError');
+});