Skip to content

Commit

Permalink
Normalize the URL in the baseUrl option (#579)
Browse files Browse the repository at this point in the history
Fixes #562
  • Loading branch information
szmarczak authored and sindresorhus committed Aug 23, 2018
1 parent f241936 commit c901c46
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 62 deletions.
13 changes: 13 additions & 0 deletions readme.md
Expand Up @@ -122,6 +122,19 @@ Very useful when used with `got.extend()` to create niche-specific Got instances

Can be a string or a [WHATWG `URL`](https://nodejs.org/api/url.html#url_class_url).

Backslash at the end of `baseUrl` and at the beginning of the `url` argument is optional:

```js
await got('hello', {baseUrl: 'https://example.com/v1'});
//=> 'https://example.com/v1/hello'

await got('/hello', {baseUrl: 'https://example.com/v1/'});
//=> 'https://example.com/v1/hello'

await got('/hello', {baseUrl: 'https://example.com/v1'});
//=> 'https://example.com/v1/hello'
```

###### headers

Type: `Object`<br>
Expand Down
1 change: 1 addition & 0 deletions source/create.js
Expand Up @@ -21,6 +21,7 @@ const aliases = [

const create = defaults => {
defaults = merge({}, defaults);
defaults.options = normalizeArguments.preNormalize(defaults.options);
if (!defaults.handler) {
defaults.handler = next;
}
Expand Down
16 changes: 5 additions & 11 deletions source/merge-instances.js
Expand Up @@ -12,17 +12,11 @@ module.exports = (instances, methods) => {
options = merge({}, options, instance.defaults.options);

const instanceHooks = instance.defaults.options.hooks;
if (instanceHooks) {
for (const name of knownHookEvents) {
if (!instanceHooks[name]) {
continue;
}

if (hooks[name]) {
hooks[name] = hooks[name].concat(instanceHooks[name]);
} else {
hooks[name] = [...instanceHooks[name]];
}
for (const name of knownHookEvents) {
if (hooks[name]) {
hooks[name] = hooks[name].concat(instanceHooks[name]);
} else {
hooks[name] = [...instanceHooks[name]];
}
}
}
Expand Down
115 changes: 68 additions & 47 deletions source/normalize-arguments.js
Expand Up @@ -13,6 +13,49 @@ const knownHookEvents = require('./known-hook-events');

const retryAfterStatusCodes = new Set([413, 429, 503]);

// `preNormalize` handles things related to static options, like `baseUrl`, `followRedirect`, `hooks`, etc.
// While `normalize` does `preNormalize` + handles things related to dynamic options, like URL, headers, body, etc.
const preNormalize = options => {
options = {
headers: {},
...options
};

if (options.baseUrl && !options.baseUrl.toString().endsWith('/')) {
options.baseUrl += '/';
}

if (is.undefined(options.followRedirect)) {
options.followRedirect = true;
}

if (is.nullOrUndefined(options.hooks)) {
options.hooks = {};
}
if (is.object(options.hooks)) {
for (const hookEvent of knownHookEvents) {
const hooks = options.hooks[hookEvent];
if (is.nullOrUndefined(hooks)) {
options.hooks[hookEvent] = [];
} else if (is.array(hooks)) {
for (const [index, hook] of hooks.entries()) {
if (!is.function(hook)) {
throw new TypeError(
`Parameter \`hooks.${hookEvent}[${index}]\` must be a function, not ${is(hook)}`
);
}
}
} else {
throw new TypeError(`Parameter \`hooks.${hookEvent}\` must be an array, not ${is(hooks)}`);
}
}
} else {
throw new TypeError(`Parameter \`hooks\` must be an object, not ${is(options.hooks)}`);
}

return options;
};

module.exports = (url, options, defaults) => {
if (Reflect.has(options, 'url') || (is.object(url) && Reflect.has(url, 'url'))) {
throw new TypeError('Parameter `url` is not an option. Use got(url, options)');
Expand All @@ -22,8 +65,14 @@ module.exports = (url, options, defaults) => {
throw new TypeError(`Parameter \`url\` must be a string or object, not ${is(url)}`);
}

options = preNormalize(options);

if (is.string(url)) {
if (options.baseUrl) {
if (url.toString().startsWith('/')) {
url = url.toString().slice(1);
}

url = urlToOptions(new URLGlobal(url, options.baseUrl));
} else {
url = url.replace(/^unix:/, 'http://$&');
Expand All @@ -45,7 +94,6 @@ module.exports = (url, options, defaults) => {

options = {
path: '',
headers: {},
...url,
protocol: url.protocol || 'http:', // Override both null/undefined with default protocol
...options
Expand All @@ -59,22 +107,17 @@ module.exports = (url, options, defaults) => {
get: () => baseUrl
});

if (options.stream && options.json) {
options.json = false;
}

if (options.decompress && is.undefined(options.headers['accept-encoding'])) {
options.headers['accept-encoding'] = 'gzip, deflate';
}

const {query} = options;

if (!is.empty(query) || query instanceof URLSearchParamsGlobal) {
const queryParams = new URLSearchParamsGlobal(query);
options.path = `${options.path.split('?')[0]}?${queryParams.toString()}`;
delete options.query;
}

if (options.stream && options.json) {
options.json = false;
}

if (options.json && is.undefined(options.headers.accept)) {
options.headers.accept = 'application/json';
}
Expand Down Expand Up @@ -125,6 +168,10 @@ module.exports = (url, options, defaults) => {

options.method = options.method.toUpperCase();

if (options.decompress && is.undefined(options.headers['accept-encoding'])) {
options.headers['accept-encoding'] = 'gzip, deflate';
}

if (options.hostname === 'unix') {
const matches = /(.+?):(.+)/.exec(options.path);

Expand Down Expand Up @@ -164,6 +211,15 @@ module.exports = (url, options, defaults) => {
}
}

if (is.number(options.timeout) || is.object(options.timeout)) {
if (is.number(options.timeout)) {
options.gotTimeout = {request: options.timeout};
} else {
options.gotTimeout = options.timeout;
}
delete options.timeout;
}

if (!is.function(options.gotRetry.retries)) {
const {retries} = options.gotRetry;

Expand Down Expand Up @@ -197,42 +253,7 @@ module.exports = (url, options, defaults) => {
};
}

if (is.undefined(options.followRedirect)) {
options.followRedirect = true;
}

if (is.number(options.timeout) || is.object(options.timeout)) {
if (is.number(options.timeout)) {
options.gotTimeout = {request: options.timeout};
} else {
options.gotTimeout = options.timeout;
}
delete options.timeout;
}

if (is.nullOrUndefined(options.hooks)) {
options.hooks = {};
}
if (is.object(options.hooks)) {
for (const hookEvent of knownHookEvents) {
const hooks = options.hooks[hookEvent];
if (is.nullOrUndefined(hooks)) {
options.hooks[hookEvent] = [];
} else if (is.array(hooks)) {
for (const [index, hook] of hooks.entries()) {
if (!is.function(hook)) {
throw new TypeError(
`Parameter \`hooks.${hookEvent}[${index}]\` must be a function, not ${is(hook)}`
);
}
}
} else {
throw new TypeError(`Parameter \`hooks.${hookEvent}\` must be an array, not ${is(hooks)}`);
}
}
} else {
throw new TypeError(`Parameter \`hooks\` must be an object, not ${is(options.hooks)}`);
}

return options;
};

module.exports.preNormalize = preNormalize;
28 changes: 28 additions & 0 deletions test/arguments.js
Expand Up @@ -22,6 +22,10 @@ test.before('setup', async () => {
response.end(request.url);
});

s.on('/test/foobar', (request, response) => {
response.end(request.url);
});

s.on('/?test=it鈥檚+ok', (request, response) => {
response.end(request.url);
});
Expand Down Expand Up @@ -194,6 +198,30 @@ test('allows extra keys in `hooks`', async t => {
await t.notThrowsAsync(() => got(`${s.url}/test`, {hooks: {extra: {}}}));
});

test('baseUrl works', async t => {
const instanceA = got.extend({baseUrl: `${s.url}/test`});
const {body} = await instanceA('/foobar');
t.is(body, `/test/foobar`);
});

test('accepts WHATWG URL as the baseUrl option', async t => {
const instanceA = got.extend({baseUrl: new URL(`${s.url}/test`)});
const {body} = await instanceA('/foobar');
t.is(body, `/test/foobar`);
});

test('backslash in the end of `baseUrl` is optional', async t => {
const instanceA = got.extend({baseUrl: `${s.url}/test/`});
const {body} = await instanceA('/foobar');
t.is(body, `/test/foobar`);
});

test('backslash in the beginning of `url` is optional when using baseUrl', async t => {
const instanceA = got.extend({baseUrl: `${s.url}/test`});
const {body} = await instanceA('foobar');
t.is(body, `/test/foobar`);
});

test('throws when trying to modify baseUrl after options got normalized', async t => {
const instanceA = got.create({
methods: [],
Expand Down
8 changes: 4 additions & 4 deletions test/create.js
Expand Up @@ -97,7 +97,7 @@ test('extend keeps the old value if the new one is undefined', t => {
test('extend merges URL instances', t => {
const a = got.extend({baseUrl: new URL('https://example.com')});
const b = a.extend({baseUrl: '/foo'});
t.is(b.defaults.options.baseUrl.toString(), 'https://example.com/foo');
t.is(b.defaults.options.baseUrl.toString(), 'https://example.com/foo/');
});

test('create', async t => {
Expand Down Expand Up @@ -128,7 +128,7 @@ test('no tampering with defaults', t => {
const instance = got.create({
handler: got.defaults.handler,
options: got.mergeOptions(got.defaults.options, {
baseUrl: 'example'
baseUrl: 'example/'
})
});

Expand All @@ -142,8 +142,8 @@ test('no tampering with defaults', t => {
instance.defaults.options.baseUrl = 'http://google.com';
});

t.is(instance.defaults.options.baseUrl, 'example');
t.is(instance2.defaults.options.baseUrl, 'example');
t.is(instance.defaults.options.baseUrl, 'example/');
t.is(instance2.defaults.options.baseUrl, 'example/');
});

test('only plain objects are freezed', async t => {
Expand Down

1 comment on commit c901c46

@szmarczak
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed:

c901c46#diff-317baa8eeebde3173dd9431bd1026adbL15

c901c46#diff-317baa8eeebde3173dd9431bd1026adbL17

because since now got.create() normalizes defaults.options too.
So hooks are normalized: it's always an object containing an array of known hooks:

hooks: {
	beforeRequest: []
}

Please sign in to comment.