diff --git a/advanced-creation.md b/advanced-creation.md
index e781a91e9..0bef1fce7 100644
--- a/advanced-creation.md
+++ b/advanced-creation.md
@@ -11,7 +11,7 @@ Configure a new `got` instance with the provided settings.
##### [options](readme.md#options)
-To inherit from parent, set it as `got.defaults.options` or use [`got.mergeOptions(defaults.options, options)`](readme.md#gotmergeOptionsparentoptions-newoptions).
+To inherit from parent, set it as `got.defaults.options` or use [`got.mergeOptions(defaults.options, options)`](readme.md#gotmergeoptionsparentoptions-newoptions).
**Note**: Avoid using [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) as it doesn't work recursively.
##### methods
diff --git a/readme.md b/readme.md
index 9982cda27..b028cb98b 100644
--- a/readme.md
+++ b/readme.md
@@ -124,7 +124,7 @@ Default: `{}`
Request headers.
-Existing headers will be overwritten. Headers set to `null` or `undefined` will be omitted.
+Existing headers will be overwritten. Headers set to `null` will be omitted.
###### stream
@@ -358,7 +358,7 @@ Sets `options.method` to the method name and makes a request.
#### got.extend([options])
-Configure a new `got` instance with default `options`. `options` are merged with the extended instance's `defaults.options` as described in [`got.mergeOptions`](#gotmergeoptionsparentoptions-newoptions).
+Configure a new `got` instance with default `options`. `options` are merged with the parent instance's `defaults.options` using [`got.mergeOptions`](#gotmergeoptionsparentoptions-newoptions).
```js
@@ -403,30 +403,28 @@ client.get('/demo');
*Need more control over the behavior of Got? Check out the [`got.create()`](advanced-creation.md).*
-**Both `got.extend(options)` and `got.create(options)` will freeze the instance's default options. For `got.extend()`, the instance's default options are the result of `got.mergeOptions`, which effectively copies plain `Object` and `Array` values. Therefore, you should treat objects passed to these methods as immutable.**
-
#### got.mergeOptions(parentOptions, newOptions)
-Extends parent options. The options objects are deeply merged to a new object. The value of each property is determined as follows:
-
-- If the new value is `undefined` the parent value is preserved.
-- If the parent value is an instance of `URL` and the new value is a `string` or `URL`, a new URL instance is created, using the parent value as the base: `new URL(new, parent)`.
-- If the new value is an `Array`, the new value is recursively merged into an empty array (the source value is discarded). `undefined` elements in the source array are not assigned during the merge, so the resulting array will have an empty item where the source array had an `undefined` item.
-- If the new value is a plain `Object`
- - If the parent value is a plain `Object`, both values are merged recursively into a new `Object`.
- - Otherwise, only the new value is merged recursively into a new `Object`.
-- Otherwise, the new value is assigned to the property.
-
-Avoid using [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) as it doesn't work recursively:
+Extends parent options. Avoid using [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) as it doesn't work recursively:
```js
-const a = {headers: {cat: 'meow', habitat: ['house', 'alley']}};
-const b = {headers: {cow: 'moo', habitat: ['barn']}};
+const a = {headers: {cat: 'meow', wolf: ['bark', 'wrrr']}};
+const b = {headers: {cow: 'moo', wolf: ['auuu']}};
-{...a, ...b} // => {headers: {cow: 'moo'}}
-got.mergeOptions(a, b) // => {headers: {cat: 'meow', cow: 'moo', habitat: ['barn']}}
+{...a, ...b} // => {headers: {cow: 'moo', wolf: ['auuu']}}
+got.mergeOptions(a, b) // => {headers: {cat: 'meow', cow: 'moo', wolf: ['auuu']}}
```
+Options are deeply merged to a new object. The value of each key is determined as follows:
+
+- If the new property is set to `undefined`, it keeps the old one.
+- If the parent property is an instance of `URL` and the new value is a `string` or `URL`, a new URL instance is created: [`new URL(new, parent)`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL#Syntax).
+- If the new property is a plain `Object`:
+ - If the parent property is a plain `Object` too, both values are merged recursively into a new `Object`.
+ - Otherwise, only the new value is deeply cloned.
+- If the new property is an `Array`, it overwrites the old one with a deep clone of the new property.
+- Otherwise, the new value is assigned to the key.
+
## Errors
Each error contains (if available) `statusCode`, `statusMessage`, `host`, `hostname`, `method`, `path`, `protocol` and `url` properties to make debugging easier.
diff --git a/source/create.js b/source/create.js
index 2cca4a7dd..1d86b8a09 100644
--- a/source/create.js
+++ b/source/create.js
@@ -1,16 +1,18 @@
'use strict';
const errors = require('./errors');
-const mergeOptions = require('./merge-options');
const asStream = require('./as-stream');
const asPromise = require('./as-promise');
const normalizeArguments = require('./normalize-arguments');
+const merge = require('./merge');
const deepFreeze = require('./deep-freeze');
const next = options => options.stream ? asStream(options) : asPromise(options);
+const mergeOptions = (defaults, options = {}) => merge({}, defaults, options);
const create = defaults => {
+ defaults = merge({}, defaults);
if (!defaults.handler) {
- defaults.handler = (options, next) => next(options);
+ defaults.handler = next;
}
function got(url, options) {
diff --git a/source/merge-options.js b/source/merge.js
similarity index 52%
rename from source/merge-options.js
rename to source/merge.js
index 91d231c68..140230740 100644
--- a/source/merge-options.js
+++ b/source/merge.js
@@ -1,25 +1,16 @@
+'use strict';
const {URL} = require('url');
const is = require('@sindresorhus/is');
-module.exports = (defaults, options = {}) => {
- return merge({}, defaults, options);
-};
-
-function merge(target, ...sources) {
+const merge = (target, ...sources) => {
for (const source of sources) {
- const sourceIter = is.array(source) ?
- source.entries() :
- Object.entries(source);
- for (const [key, sourceValue] of sourceIter) {
- const targetValue = target[key];
+ for (const [key, sourceValue] of Object.entries(source)) {
if (is.undefined(sourceValue)) {
continue;
}
- if (is.array(sourceValue)) {
- target[key] = merge(new Array(sourceValue.length), sourceValue);
- } else if (is.urlInstance(targetValue) && (
- is.urlInstance(sourceValue) || is.string(sourceValue)
- )) {
+
+ const targetValue = target[key];
+ if (is.urlInstance(targetValue) && (is.urlInstance(sourceValue) || is.string(sourceValue))) {
target[key] = new URL(sourceValue, targetValue);
} else if (is.plainObject(sourceValue)) {
if (is.plainObject(targetValue)) {
@@ -27,10 +18,15 @@ function merge(target, ...sources) {
} else {
target[key] = merge({}, sourceValue);
}
+ } else if (is.array(sourceValue)) {
+ target[key] = merge([], sourceValue);
} else {
target[key] = sourceValue;
}
}
}
+
return target;
-}
+};
+
+module.exports = merge;
diff --git a/test/create.js b/test/create.js
index 4004757b3..bcd60c51d 100644
--- a/test/create.js
+++ b/test/create.js
@@ -77,26 +77,18 @@ test('custom headers (extend)', async t => {
t.is(headers.unicorn, 'rainbow');
});
-test('extend overwrites arrays', t => {
+test('extend overwrites arrays with a deep clone', t => {
const statusCodes = [408];
const a = got.extend({retry: {statusCodes}});
- t.deepEqual(a.defaults.options.retry.statusCodes, statusCodes);
+ statusCodes[0] = 500;
+ t.deepEqual(a.defaults.options.retry.statusCodes, [408]);
t.not(a.defaults.options.retry.statusCodes, statusCodes);
});
-test('extend overwrites null', t => {
- const statusCodes = null;
- const a = got.extend({retry: {statusCodes}});
- t.is(a.defaults.options.retry.statusCodes, statusCodes);
-});
-
-test('extend ignores source values set to undefined', t => {
- const a = got.extend({
- headers: {foo: undefined, 'user-agent': undefined}
- });
- const b = a.extend({headers: {foo: undefined}});
+test('extend keeps the old value if the new one is undefined', t => {
+ const a = got.extend({headers: undefined});
t.deepEqual(
- b.defaults.options.headers,
+ a.defaults.options.headers,
got.defaults.options.headers
);
});
@@ -107,12 +99,6 @@ test('extend merges URL instances', t => {
t.is(b.defaults.options.baseUrl.toString(), 'https://example.com/foo');
});
-test('extend ignores object values set to undefined (root keys)', t => {
- t.true(Reflect.has(got.defaults.options, 'headers'));
- const a = got.extend({headers: undefined});
- t.deepEqual(a.defaults.options, got.defaults.options);
-});
-
test('create', async t => {
const instance = got.create({
options: {},
@@ -161,3 +147,20 @@ test('no tampering with defaults', t => {
t.is(instance.defaults.options.baseUrl, 'example');
t.is(instance2.defaults.options.baseUrl, 'example');
});
+
+test('defaults are cloned on instance creation', t => {
+ const options = {foo: 'bar'};
+ const methods = ['get'];
+ const instance = got.create({
+ methods,
+ options
+ });
+
+ t.notThrows(() => {
+ options.foo = 'foo';
+ methods[0] = 'post';
+ });
+
+ t.not(options.foo, instance.defaults.options.foo);
+ t.not(methods[0], instance.defaults.methods[0]);
+});
diff --git a/test/headers.js b/test/headers.js
index c95a726d5..8c8319a63 100644
--- a/test/headers.js
+++ b/test/headers.js
@@ -142,12 +142,22 @@ test('remove null value headers', async t => {
t.false(Reflect.has(headers, 'user-agent'));
});
-test('remove undefined value headers', async t => {
+test('setting a header to undefined keeps the old value', async t => {
const {body} = await got(s.url, {
headers: {
- foo: undefined
+ 'user-agent': undefined
}
});
const headers = JSON.parse(body);
- t.false(Reflect.has(headers, 'foo'));
+ t.not(headers['user-agent'], undefined);
+});
+
+test('non-existent headers set to undefined are omitted', async t => {
+ const {body} = await got(s.url, {
+ headers: {
+ blah: undefined
+ }
+ });
+ const headers = JSON.parse(body);
+ t.false(Reflect.has(headers, 'blah'));
});