Skip to content

Commit f5c54a3

Browse files
committed
Improve validation error messages by including option names
Fixes #1257
1 parent a186d20 commit f5c54a3

File tree

4 files changed

+110
-68
lines changed

4 files changed

+110
-68
lines changed

source/core/options.ts

Lines changed: 95 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -763,12 +763,46 @@ export type PaginationOptions<ElementType, BodyType> = {
763763

764764
export type SearchParameters = Record<string, string | number | boolean | null | undefined>; // eslint-disable-line @typescript-eslint/ban-types
765765

766+
/**
767+
Generic helper that wraps any assertion function to add context to error messages.
768+
*/
769+
function wrapAssertionWithContext(optionName: string, assertionFn: () => void): void {
770+
try {
771+
assertionFn();
772+
} catch (error) {
773+
if (error instanceof Error) {
774+
error.message = `Option '${optionName}': ${error.message}`;
775+
}
776+
777+
throw error;
778+
}
779+
}
780+
781+
/**
782+
Helper function that wraps assert.any() to provide better error messages.
783+
When assertion fails, it includes the option name in the error message.
784+
*/
785+
function assertAny(optionName: string, validators: any[], value: unknown): void {
786+
wrapAssertionWithContext(optionName, () => {
787+
assert.any(validators, value);
788+
});
789+
}
790+
791+
/**
792+
Helper function that wraps assert.plainObject() to provide better error messages.
793+
When assertion fails, it includes the option name in the error message.
794+
*/
795+
function assertPlainObject(optionName: string, value: unknown): void {
796+
wrapAssertionWithContext(optionName, () => {
797+
assert.plainObject(value);
798+
});
799+
}
800+
766801
function validateSearchParameters(searchParameters: Record<string, unknown>): asserts searchParameters is Record<string, string | number | boolean | null | undefined> { // eslint-disable-line @typescript-eslint/ban-types
767802
// eslint-disable-next-line guard-for-in
768803
for (const key in searchParameters) {
769804
const value = searchParameters[key];
770-
771-
assert.any([is.string, is.number, is.boolean, is.null, is.undefined], value);
805+
assertAny(`searchParams.${key}`, [is.string, is.number, is.boolean, is.null, is.undefined], value);
772806
}
773807
}
774808

@@ -1162,9 +1196,9 @@ export default class Options {
11621196
private readonly _init: OptionsInit[];
11631197

11641198
constructor(input?: string | URL | OptionsInit, options?: OptionsInit, defaults?: Options) {
1165-
assert.any([is.string, is.urlInstance, is.object, is.undefined], input);
1166-
assert.any([is.object, is.undefined], options);
1167-
assert.any([is.object, is.undefined], defaults);
1199+
assertAny('input', [is.string, is.urlInstance, is.object, is.undefined], input);
1200+
assertAny('options', [is.object, is.undefined], options);
1201+
assertAny('defaults', [is.object, is.undefined], defaults);
11681202

11691203
if (input instanceof Options || options instanceof Options) {
11701204
throw new TypeError('The defaults must be passed as the third argument');
@@ -1297,7 +1331,7 @@ export default class Options {
12971331
}
12981332

12991333
set request(value: RequestFunction | undefined) {
1300-
assert.any([is.function, is.undefined], value);
1334+
assertAny('request', [is.function, is.undefined], value);
13011335

13021336
this._internals.request = value;
13031337
}
@@ -1329,7 +1363,7 @@ export default class Options {
13291363
}
13301364

13311365
set agent(value: Agents) {
1332-
assert.plainObject(value);
1366+
assertPlainObject('agent', value);
13331367

13341368
// eslint-disable-next-line guard-for-in
13351369
for (const key in value) {
@@ -1338,7 +1372,7 @@ export default class Options {
13381372
}
13391373

13401374
// @ts-expect-error - No idea why `value[key]` doesn't work here.
1341-
assert.any([is.object, is.undefined, (v: unknown) => v === false], value[key]);
1375+
assertAny(`agent.${key}`, [is.object, is.undefined, (v: unknown) => v === false], value[key]);
13421376
}
13431377

13441378
if (this._merging) {
@@ -1398,7 +1432,7 @@ export default class Options {
13981432
}
13991433

14001434
set timeout(value: Delays) {
1401-
assert.plainObject(value);
1435+
assertPlainObject('timeout', value);
14021436

14031437
// eslint-disable-next-line guard-for-in
14041438
for (const key in value) {
@@ -1407,7 +1441,7 @@ export default class Options {
14071441
}
14081442

14091443
// @ts-expect-error - No idea why `value[key]` doesn't work here.
1410-
assert.any([is.number, is.undefined], value[key]);
1444+
assertAny(`timeout.${key}`, [is.number, is.undefined], value[key]);
14111445
}
14121446

14131447
if (this._merging) {
@@ -1463,7 +1497,7 @@ export default class Options {
14631497
}
14641498

14651499
set prefixUrl(value: string | URL) {
1466-
assert.any([is.string, is.urlInstance], value);
1500+
assertAny('prefixUrl', [is.string, is.urlInstance], value);
14671501

14681502
if (value === '') {
14691503
this._internals.prefixUrl = '';
@@ -1520,7 +1554,7 @@ export default class Options {
15201554
}
15211555

15221556
set body(value: string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | ArrayBufferView | undefined) {
1523-
assert.any([is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, is.iterable, is.asyncIterable, isFormData, is.typedArray, is.undefined], value);
1557+
assertAny('body', [is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, is.iterable, is.asyncIterable, isFormData, is.typedArray, is.undefined], value);
15241558

15251559
if (is.nodeStream(value)) {
15261560
assert.truthy(value.readable);
@@ -1548,7 +1582,7 @@ export default class Options {
15481582
}
15491583

15501584
set form(value: Record<string, any> | undefined) {
1551-
assert.any([is.plainObject, is.undefined], value);
1585+
assertAny('form', [is.plainObject, is.undefined], value);
15521586

15531587
if (value !== undefined) {
15541588
assert.undefined(this._internals.body);
@@ -1603,7 +1637,7 @@ export default class Options {
16031637
}
16041638

16051639
set url(value: string | URL | undefined) {
1606-
assert.any([is.string, is.urlInstance, is.undefined], value);
1640+
assertAny('url', [is.string, is.urlInstance, is.undefined], value);
16071641

16081642
if (value === undefined) {
16091643
this._internals.url = undefined;
@@ -1679,7 +1713,7 @@ export default class Options {
16791713
}
16801714

16811715
set cookieJar(value: PromiseCookieJar | ToughCookieJar | undefined) {
1682-
assert.any([is.object, is.undefined], value);
1716+
assertAny('cookieJar', [is.object, is.undefined], value);
16831717

16841718
if (value === undefined) {
16851719
this._internals.cookieJar = undefined;
@@ -1780,7 +1814,7 @@ export default class Options {
17801814
}
17811815

17821816
set searchParams(value: string | SearchParameters | URLSearchParams | undefined) {
1783-
assert.any([is.string, is.object, is.undefined], value);
1817+
assertAny('searchParams', [is.string, is.object, is.undefined], value);
17841818

17851819
const url = this._internals.url as URL;
17861820

@@ -1849,7 +1883,7 @@ export default class Options {
18491883
}
18501884

18511885
set dnsLookup(value: CacheableLookup['lookup'] | undefined) {
1852-
assert.any([is.function, is.undefined], value);
1886+
assertAny('dnsLookup', [is.function, is.undefined], value);
18531887

18541888
this._internals.dnsLookup = value;
18551889
}
@@ -1869,7 +1903,7 @@ export default class Options {
18691903
}
18701904

18711905
set dnsCache(value: CacheableLookup | boolean | undefined) {
1872-
assert.any([is.object, is.boolean, is.undefined], value);
1906+
assertAny('dnsCache', [is.object, is.boolean, is.undefined], value);
18731907

18741908
if (value === true) {
18751909
this._internals.dnsCache = getGlobalDnsCache();
@@ -1945,7 +1979,7 @@ export default class Options {
19451979
const typedKnownHookEvent = knownHookEvent as keyof Hooks;
19461980
const hooks = value[typedKnownHookEvent];
19471981

1948-
assert.any([is.array, is.undefined], hooks);
1982+
assertAny(`hooks.${knownHookEvent}`, [is.array, is.undefined], hooks);
19491983

19501984
if (hooks) {
19511985
for (const hook of hooks) {
@@ -1984,7 +2018,7 @@ export default class Options {
19842018
}
19852019

19862020
set followRedirect(value: boolean | ((response: PlainResponse) => boolean)) {
1987-
assert.any([is.boolean, is.function], value);
2021+
assertAny('followRedirect', [is.boolean, is.function], value);
19882022

19892023
this._internals.followRedirect = value;
19902024
}
@@ -2022,7 +2056,7 @@ export default class Options {
20222056
}
20232057

20242058
set cache(value: string | StorageAdapter | boolean | undefined) {
2025-
assert.any([is.object, is.string, is.boolean, is.undefined], value);
2059+
assertAny('cache', [is.object, is.string, is.boolean, is.undefined], value);
20262060

20272061
if (value === true) {
20282062
this._internals.cache = globalCache;
@@ -2156,7 +2190,7 @@ export default class Options {
21562190
}
21572191

21582192
set headers(value: Headers) {
2159-
assert.plainObject(value);
2193+
assertPlainObject('headers', value);
21602194

21612195
if (this._merging) {
21622196
Object.assign(this._internals.headers, lowercaseKeys(value));
@@ -2314,16 +2348,16 @@ export default class Options {
23142348
}
23152349

23162350
set retry(value: Partial<RetryOptions>) {
2317-
assert.plainObject(value);
2351+
assertPlainObject('retry', value);
23182352

2319-
assert.any([is.function, is.undefined], value.calculateDelay);
2320-
assert.any([is.number, is.undefined], value.maxRetryAfter);
2321-
assert.any([is.number, is.undefined], value.limit);
2322-
assert.any([is.array, is.undefined], value.methods);
2323-
assert.any([is.array, is.undefined], value.statusCodes);
2324-
assert.any([is.array, is.undefined], value.errorCodes);
2325-
assert.any([is.number, is.undefined], value.noise);
2326-
assert.any([is.boolean, is.undefined], value.enforceRetryRules);
2353+
assertAny('retry.calculateDelay', [is.function, is.undefined], value.calculateDelay);
2354+
assertAny('retry.maxRetryAfter', [is.number, is.undefined], value.maxRetryAfter);
2355+
assertAny('retry.limit', [is.number, is.undefined], value.limit);
2356+
assertAny('retry.methods', [is.array, is.undefined], value.methods);
2357+
assertAny('retry.statusCodes', [is.array, is.undefined], value.statusCodes);
2358+
assertAny('retry.errorCodes', [is.array, is.undefined], value.errorCodes);
2359+
assertAny('retry.noise', [is.number, is.undefined], value.noise);
2360+
assertAny('retry.enforceRetryRules', [is.boolean, is.undefined], value.enforceRetryRules);
23272361

23282362
if (value.noise && Math.abs(value.noise) > 100) {
23292363
throw new Error(`The maximum acceptable retry noise is +/- 100ms, got ${value.noise}`);
@@ -2358,7 +2392,7 @@ export default class Options {
23582392
}
23592393

23602394
set localAddress(value: string | undefined) {
2361-
assert.any([is.string, is.undefined], value);
2395+
assertAny('localAddress', [is.string, is.undefined], value);
23622396

23632397
this._internals.localAddress = value;
23642398
}
@@ -2383,7 +2417,7 @@ export default class Options {
23832417
}
23842418

23852419
set createConnection(value: CreateConnectionFunction | undefined) {
2386-
assert.any([is.function, is.undefined], value);
2420+
assertAny('createConnection', [is.function, is.undefined], value);
23872421

23882422
this._internals.createConnection = value;
23892423
}
@@ -2398,12 +2432,12 @@ export default class Options {
23982432
}
23992433

24002434
set cacheOptions(value: CacheOptions) {
2401-
assert.plainObject(value);
2435+
assertPlainObject('cacheOptions', value);
24022436

2403-
assert.any([is.boolean, is.undefined], value.shared);
2404-
assert.any([is.number, is.undefined], value.cacheHeuristic);
2405-
assert.any([is.number, is.undefined], value.immutableMinTimeToLive);
2406-
assert.any([is.boolean, is.undefined], value.ignoreCargoCult);
2437+
assertAny('cacheOptions.shared', [is.boolean, is.undefined], value.shared);
2438+
assertAny('cacheOptions.cacheHeuristic', [is.number, is.undefined], value.cacheHeuristic);
2439+
assertAny('cacheOptions.immutableMinTimeToLive', [is.number, is.undefined], value.immutableMinTimeToLive);
2440+
assertAny('cacheOptions.ignoreCargoCult', [is.boolean, is.undefined], value.ignoreCargoCult);
24072441

24082442
for (const key in value) {
24092443
if (!(key in this._internals.cacheOptions)) {
@@ -2426,27 +2460,27 @@ export default class Options {
24262460
}
24272461

24282462
set https(value: HttpsOptions) {
2429-
assert.plainObject(value);
2430-
2431-
assert.any([is.boolean, is.undefined], value.rejectUnauthorized);
2432-
assert.any([is.function, is.undefined], value.checkServerIdentity);
2433-
assert.any([is.string, is.undefined], value.serverName);
2434-
assert.any([is.string, is.object, is.array, is.undefined], value.certificateAuthority);
2435-
assert.any([is.string, is.object, is.array, is.undefined], value.key);
2436-
assert.any([is.string, is.object, is.array, is.undefined], value.certificate);
2437-
assert.any([is.string, is.undefined], value.passphrase);
2438-
assert.any([is.string, is.buffer, is.array, is.undefined], value.pfx);
2439-
assert.any([is.array, is.undefined], value.alpnProtocols);
2440-
assert.any([is.string, is.undefined], value.ciphers);
2441-
assert.any([is.string, is.buffer, is.undefined], value.dhparam);
2442-
assert.any([is.string, is.undefined], value.signatureAlgorithms);
2443-
assert.any([is.string, is.undefined], value.minVersion);
2444-
assert.any([is.string, is.undefined], value.maxVersion);
2445-
assert.any([is.boolean, is.undefined], value.honorCipherOrder);
2446-
assert.any([is.number, is.undefined], value.tlsSessionLifetime);
2447-
assert.any([is.string, is.undefined], value.ecdhCurve);
2448-
assert.any([is.string, is.buffer, is.array, is.undefined], value.certificateRevocationLists);
2449-
assert.any([is.number, is.undefined], value.secureOptions);
2463+
assertPlainObject('https', value);
2464+
2465+
assertAny('https.rejectUnauthorized', [is.boolean, is.undefined], value.rejectUnauthorized);
2466+
assertAny('https.checkServerIdentity', [is.function, is.undefined], value.checkServerIdentity);
2467+
assertAny('https.serverName', [is.string, is.undefined], value.serverName);
2468+
assertAny('https.certificateAuthority', [is.string, is.object, is.array, is.undefined], value.certificateAuthority);
2469+
assertAny('https.key', [is.string, is.object, is.array, is.undefined], value.key);
2470+
assertAny('https.certificate', [is.string, is.object, is.array, is.undefined], value.certificate);
2471+
assertAny('https.passphrase', [is.string, is.undefined], value.passphrase);
2472+
assertAny('https.pfx', [is.string, is.buffer, is.array, is.undefined], value.pfx);
2473+
assertAny('https.alpnProtocols', [is.array, is.undefined], value.alpnProtocols);
2474+
assertAny('https.ciphers', [is.string, is.undefined], value.ciphers);
2475+
assertAny('https.dhparam', [is.string, is.buffer, is.undefined], value.dhparam);
2476+
assertAny('https.signatureAlgorithms', [is.string, is.undefined], value.signatureAlgorithms);
2477+
assertAny('https.minVersion', [is.string, is.undefined], value.minVersion);
2478+
assertAny('https.maxVersion', [is.string, is.undefined], value.maxVersion);
2479+
assertAny('https.honorCipherOrder', [is.boolean, is.undefined], value.honorCipherOrder);
2480+
assertAny('https.tlsSessionLifetime', [is.number, is.undefined], value.tlsSessionLifetime);
2481+
assertAny('https.ecdhCurve', [is.string, is.undefined], value.ecdhCurve);
2482+
assertAny('https.certificateRevocationLists', [is.string, is.buffer, is.array, is.undefined], value.certificateRevocationLists);
2483+
assertAny('https.secureOptions', [is.number, is.undefined], value.secureOptions);
24502484

24512485
for (const key in value) {
24522486
if (!(key in this._internals.https)) {
@@ -2480,7 +2514,7 @@ export default class Options {
24802514
throw new TypeError('To get a Buffer, set `options.responseType` to `buffer` instead');
24812515
}
24822516

2483-
assert.any([is.string, is.undefined], value);
2517+
assertAny('encoding', [is.string, is.undefined], value);
24842518

24852519
this._internals.encoding = value;
24862520
}
@@ -2600,7 +2634,7 @@ export default class Options {
26002634
}
26012635

26022636
set maxHeaderSize(value: number | undefined) {
2603-
assert.any([is.number, is.undefined], value);
2637+
assertAny('maxHeaderSize', [is.number, is.undefined], value);
26042638

26052639
this._internals.maxHeaderSize = value;
26062640
}

source/create.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,15 @@ const create = (defaults: InstanceDefaults): Got => {
192192
} else {
193193
normalizedOptions.merge(optionsToMerge);
194194

195-
assert.any([is.urlInstance, is.undefined], optionsToMerge.url);
195+
try {
196+
assert.any([is.urlInstance, is.undefined], optionsToMerge.url);
197+
} catch (error) {
198+
if (error instanceof Error) {
199+
error.message = `Option 'pagination.paginate.url': ${error.message}`;
200+
}
201+
202+
throw error;
203+
}
196204

197205
if (optionsToMerge.url !== undefined) {
198206
normalizedOptions.prefixUrl = '';

0 commit comments

Comments
 (0)