Description
Config type accepts ky options, the runtime silently drops
Config is declared as:
export interface Config<T extends ClientOptions = ClientOptions>
extends
Omit<KyOptions, 'body' | 'headers' | 'method' | 'prefixUrl' | 'retry' | 'throwHttpErrors'>,
CoreConfig {
Only six fields are stripped. Every other KyOptions field — including ky-only extras — is inherited into Config.
But the runtime in request() only hand-maps a specific RequestInit-like subset into the per-call kyOptions. The following are accepted by the type but never forwarded:
hooks
fetch
parseJson, stringifyJson
onDownloadProgress, onUploadProgress
context
searchParams
Thus, createClient({ hooks: { afterResponse: [...] } }) type-checks cleanly and silently does nothing at runtime. The user has to use kyOptions.hooks, but nothing in the type surface tells them that.
Explicit mapping clobbers custom ky instance defaults
The per-call kyOptions is rebuilt as a fresh object literal:
const kyOptions: KyOptions = {
body: validBody as BodyInit,
cache: opts.cache,
credentials: opts.credentials,
headers: opts.headers,
integrity: opts.integrity,
keepalive: opts.keepalive,
method: opts.method,
mode: opts.mode,
redirect: 'follow',
referrer: opts.referrer,
referrerPolicy: opts.referrerPolicy,
signal: opts.signal,
throwHttpErrors: opts.throwOnError ?? false,
timeout: opts.timeout,
...opts.kyOptions,
};
Every known field is written as its own property — even when opts.<field> is undefined. So given:
const myKy = ky.create({ credentials: 'include' });
createClient({ ky: myKy });
Every request emits credentials: undefined into kyOptions, which overrides the 'include' default the ky instance was configured with. Same for cache, mode, signal, timeout, integrity, keepalive, referrer, referrerPolicy.
Top-level retry is a lossy subset and clobbers kyOptions.retry
Exposed RetryOptions:
export interface RetryOptions {
limit?: number;
methods?: Array<'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options' | 'trace'>;
statusCodes?: number[];
}
drops ky's retryOnTimeout, delay, jitter, shouldRetry, backoffLimit. The only path to those is kyOptions.retry.
Worse, the retry mapping runs after ...opts.kyOptions is spread:
const kyOptions = {
...,
...opts.kyOptions,
};
if (opts.retry && typeof opts.retry === 'object') {
kyOptions.retry = {
limit: retryOpts.limit ?? 2,
methods: retryOpts.methods,
statusCodes: retryOpts.statusCodes,
};
}
So a user who sets both top-level retry (for the ergonomic fields) and kyOptions.retry (for retryOnTimeout) silently loses the kyOptions half. The only working combo is "use kyOptions.retry exclusively and never set top-level retry" — which is non-obvious.
prefixUrl stripped, baseUrl reimplemented in parallel
When working with custom ky, prefixUrl is being ignored. The client reimplements URL prefixing as baseUrl inside its own buildUrl:
let url = (baseUrl ?? '') + pathUrl;
The fully-resolved absolute URL is then baked into a new Request(url, ...) and that Request object is what's passed to ky. Since ky's own prefixUrl handling only activates when the input is typeof === 'string', prefixUrl on a custom ky instance is dead code — it can never fire.
Individually each issue is small. Together they make createClient({ ky: myKy }) behave very differently from what users expect:
- Fetch-level settings on the instance get clobbered (2).
prefixUrl on the instance is ignored (4).
- Lossy
retry on the instance can be wiped out by a top-level retry (3).
hooks, fetch, parseJson, etc. do survive on the instance — but those are exactly the fields the Config type currently advertises at top level and silently drops (1).
The practical result is that users reaching for ky.create() still need to duplicate most configuration at the createClient level. That inverts the composition story a custom ky instance is supposed to offer.
Reproducible example or configuration
No response
OpenAPI specification (optional)
No response
System information (optional)
No response
Description
Config type accepts ky options, the runtime silently drops
Configis declared as:Only six fields are stripped. Every other
KyOptionsfield — including ky-only extras — is inherited intoConfig.But the runtime in
request()only hand-maps a specific RequestInit-like subset into the per-callkyOptions. The following are accepted by the type but never forwarded:hooksfetchparseJson,stringifyJsononDownloadProgress,onUploadProgresscontextsearchParamsThus,
createClient({ hooks: { afterResponse: [...] } })type-checks cleanly and silently does nothing at runtime. The user has to usekyOptions.hooks, but nothing in the type surface tells them that.Explicit mapping clobbers custom
kyinstance defaultsThe per-call
kyOptionsis rebuilt as a fresh object literal:Every known field is written as its own property — even when
opts.<field>isundefined. So given:Every request emits
credentials: undefinedintokyOptions, which overrides the'include'default the ky instance was configured with. Same forcache,mode,signal,timeout,integrity,keepalive,referrer,referrerPolicy.Top-level
retryis a lossy subset and clobberskyOptions.retryExposed
RetryOptions:drops ky's
retryOnTimeout,delay,jitter,shouldRetry,backoffLimit. The only path to those iskyOptions.retry.Worse, the retry mapping runs after
...opts.kyOptionsis spread:So a user who sets both top-level
retry(for the ergonomic fields) andkyOptions.retry(forretryOnTimeout) silently loses the kyOptions half. The only working combo is "usekyOptions.retryexclusively and never set top-levelretry" — which is non-obvious.prefixUrlstripped,baseUrlreimplemented in parallelWhen working with custom
ky,prefixUrlis being ignored. The client reimplements URL prefixing asbaseUrlinside its ownbuildUrl:The fully-resolved absolute URL is then baked into a
new Request(url, ...)and that Request object is what's passed to ky. Since ky's ownprefixUrlhandling only activates when the input istypeof === 'string',prefixUrlon a custom ky instance is dead code — it can never fire.Individually each issue is small. Together they make
createClient({ ky: myKy })behave very differently from what users expect:prefixUrlon the instance is ignored (4).retryon the instance can be wiped out by a top-levelretry(3).hooks,fetch,parseJson, etc. do survive on the instance — but those are exactly the fields the Config type currently advertises at top level and silently drops (1).The practical result is that users reaching for
ky.create()still need to duplicate most configuration at thecreateClientlevel. That inverts the composition story a custom ky instance is supposed to offer.Reproducible example or configuration
No response
OpenAPI specification (optional)
No response
System information (optional)
No response