@kubb/plugin-axios 5.0.0-beta.79 → 5.0.0-beta.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +36 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +36 -2
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/plugin.ts +2 -1
- package/templates/axios.ts +272 -222
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-axios",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.81",
|
|
4
4
|
"description": "Generate a slim, type-safe HTTP client with Kubb based on axios.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-client",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"registry": "https://registry.npmjs.org/"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@kubb/ast": "5.0.0-beta.
|
|
53
|
-
"@kubb/core": "5.0.0-beta.
|
|
54
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
55
|
-
"@kubb/plugin-ts": "5.0.0-beta.
|
|
56
|
-
"@kubb/plugin-zod": "5.0.0-beta.
|
|
52
|
+
"@kubb/ast": "5.0.0-beta.80",
|
|
53
|
+
"@kubb/core": "5.0.0-beta.80",
|
|
54
|
+
"@kubb/renderer-jsx": "5.0.0-beta.80",
|
|
55
|
+
"@kubb/plugin-ts": "5.0.0-beta.81",
|
|
56
|
+
"@kubb/plugin-zod": "5.0.0-beta.81"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"axios": "^1.18.1",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@internals/utils": "0.0.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
66
|
+
"@kubb/renderer-jsx": "5.0.0-beta.80",
|
|
67
67
|
"axios": "^1.7.2"
|
|
68
68
|
},
|
|
69
69
|
"peerDependenciesMeta": {
|
package/src/plugin.ts
CHANGED
|
@@ -80,6 +80,7 @@ export const pluginAxios = definePlugin<PluginAxios>((options) => {
|
|
|
80
80
|
ctx.addGenerator(...selectedGenerators)
|
|
81
81
|
|
|
82
82
|
const root = path.resolve(ctx.config.root, ctx.config.output.path)
|
|
83
|
+
const baseURLExpression = baseURL ? (baseURL.includes('${') ? `\`${baseURL.replaceAll('`', '\\`')}\`` : JSON.stringify(baseURL)) : undefined
|
|
83
84
|
|
|
84
85
|
ctx.injectFile({
|
|
85
86
|
baseName: 'serializers.ts',
|
|
@@ -91,7 +92,7 @@ export const pluginAxios = definePlugin<PluginAxios>((options) => {
|
|
|
91
92
|
baseName: 'client.ts',
|
|
92
93
|
path: path.resolve(root, '.kubb/client.ts'),
|
|
93
94
|
copy: axiosClientTemplatePath,
|
|
94
|
-
footer:
|
|
95
|
+
footer: baseURLExpression ? `client.setConfig({ baseURL: ${baseURLExpression} })` : undefined,
|
|
95
96
|
})
|
|
96
97
|
|
|
97
98
|
ctx.injectFile({
|
package/templates/axios.ts
CHANGED
|
@@ -254,108 +254,6 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
|
|
|
254
254
|
/**
|
|
255
255
|
* Thrown for a non-2xx response, so a resolved call always means success.
|
|
256
256
|
*/
|
|
257
|
-
/**
|
|
258
|
-
* One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
|
|
259
|
-
*/
|
|
260
|
-
export type ServerSentEvent<TData = unknown> = {
|
|
261
|
-
data: TData
|
|
262
|
-
event?: string
|
|
263
|
-
id?: string
|
|
264
|
-
retry?: number
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
|
|
268
|
-
if (!('getReader' in stream)) {
|
|
269
|
-
yield* stream
|
|
270
|
-
return
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const reader = stream.getReader()
|
|
274
|
-
try {
|
|
275
|
-
while (true) {
|
|
276
|
-
const { done, value } = await reader.read()
|
|
277
|
-
if (done) return
|
|
278
|
-
yield value
|
|
279
|
-
}
|
|
280
|
-
} finally {
|
|
281
|
-
await reader.cancel().catch(() => {})
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
|
|
286
|
-
const data: Array<string> = []
|
|
287
|
-
const event: ServerSentEvent<TData> = { data: undefined as TData }
|
|
288
|
-
let seen = false
|
|
289
|
-
|
|
290
|
-
for (const line of raw.split('\n')) {
|
|
291
|
-
if (!line || line.startsWith(':')) continue
|
|
292
|
-
seen = true
|
|
293
|
-
const index = line.indexOf(':')
|
|
294
|
-
const field = index === -1 ? line : line.slice(0, index)
|
|
295
|
-
const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
|
|
296
|
-
if (field === 'data') data.push(value)
|
|
297
|
-
else if (field === 'event') event.event = value
|
|
298
|
-
else if (field === 'id') event.id = value
|
|
299
|
-
else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (!seen) return undefined
|
|
303
|
-
|
|
304
|
-
if (data.length) {
|
|
305
|
-
const joined = data.join('\n')
|
|
306
|
-
try {
|
|
307
|
-
event.data = JSON.parse(joined) as TData
|
|
308
|
-
} catch {
|
|
309
|
-
event.data = joined as TData
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return event
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
|
|
317
|
-
*/
|
|
318
|
-
export async function* parseEventStream<TData = unknown>(
|
|
319
|
-
stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
320
|
-
): AsyncGenerator<ServerSentEvent<TData>> {
|
|
321
|
-
const decoder = new TextDecoder()
|
|
322
|
-
const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
|
|
323
|
-
let buffer = ''
|
|
324
|
-
|
|
325
|
-
for await (const chunk of readBytes(stream)) {
|
|
326
|
-
const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
|
|
327
|
-
buffer = blocks.pop() ?? ''
|
|
328
|
-
for (const block of blocks) {
|
|
329
|
-
const event = parseEvent<TData>(block)
|
|
330
|
-
if (event) yield event
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
|
|
335
|
-
if (event) yield event
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* The resolved shape returned by a generated `text/event-stream` operation: the typed event
|
|
340
|
-
* `stream` plus the native `response`.
|
|
341
|
-
*/
|
|
342
|
-
export type EventStreamResult<TData = unknown, TResponse = AxiosResponse> = {
|
|
343
|
-
stream: AsyncGenerator<ServerSentEvent<TData>>
|
|
344
|
-
response: TResponse
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
|
|
349
|
-
* the parsed events as a typed async iterator. Generated SSE operations call this.
|
|
350
|
-
*/
|
|
351
|
-
export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: AxiosResponse }>): Promise<EventStreamResult<TData>> {
|
|
352
|
-
const { data, response } = await result
|
|
353
|
-
return {
|
|
354
|
-
response,
|
|
355
|
-
stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
257
|
export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> extends Error {
|
|
360
258
|
data: TError
|
|
361
259
|
status: number
|
|
@@ -416,6 +314,32 @@ function serializeUrl({
|
|
|
416
314
|
return path + (search ? `?${search}` : '')
|
|
417
315
|
}
|
|
418
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API, mapping a stable external id onto axios's own so `update` can swap a handler in place.
|
|
319
|
+
*/
|
|
320
|
+
function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
|
|
321
|
+
const ids = new Map<number, number>()
|
|
322
|
+
let counter = 0
|
|
323
|
+
return {
|
|
324
|
+
use(fn) {
|
|
325
|
+
const id = ++counter
|
|
326
|
+
ids.set(id, register(fn))
|
|
327
|
+
return id
|
|
328
|
+
},
|
|
329
|
+
eject(id) {
|
|
330
|
+
const nativeId = ids.get(id)
|
|
331
|
+
if (nativeId === undefined) return
|
|
332
|
+
ejectNative(nativeId)
|
|
333
|
+
ids.delete(id)
|
|
334
|
+
},
|
|
335
|
+
update(id, fn) {
|
|
336
|
+
const nativeId = ids.get(id)
|
|
337
|
+
if (nativeId !== undefined) ejectNative(nativeId)
|
|
338
|
+
ids.set(id, register(fn))
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
419
343
|
/**
|
|
420
344
|
* Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
|
|
421
345
|
*/
|
|
@@ -428,16 +352,23 @@ export async function resolveAuth(params: {
|
|
|
428
352
|
const { security, auth, headers, query } = params
|
|
429
353
|
if (!security?.length || auth === undefined) return
|
|
430
354
|
|
|
355
|
+
// An explicit caller-set header wins over the resolved token.
|
|
356
|
+
const hasHeader = (name: string) => Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase())
|
|
357
|
+
|
|
431
358
|
for (const scheme of security) {
|
|
432
359
|
const token = typeof auth === 'function' ? await auth(scheme) : auth
|
|
433
360
|
if (token === undefined) continue
|
|
434
361
|
|
|
435
362
|
if (scheme.type === 'apiKey') {
|
|
436
363
|
const name = scheme.name ?? 'Authorization'
|
|
437
|
-
if (scheme.in === 'query')
|
|
438
|
-
|
|
439
|
-
else
|
|
440
|
-
|
|
364
|
+
if (scheme.in === 'query') {
|
|
365
|
+
if (query[name] === undefined) query[name] = token
|
|
366
|
+
} else if (scheme.in === 'cookie') {
|
|
367
|
+
headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
|
|
368
|
+
} else if (!hasHeader(name)) {
|
|
369
|
+
headers[name] = token
|
|
370
|
+
}
|
|
371
|
+
} else if (!hasHeader('Authorization')) {
|
|
441
372
|
headers.Authorization = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}`
|
|
442
373
|
}
|
|
443
374
|
return
|
|
@@ -449,32 +380,6 @@ async function runValidator<T>(validator: Validator<T> | undefined, value: T): P
|
|
|
449
380
|
return validateStandardSchema(validator, value)
|
|
450
381
|
}
|
|
451
382
|
|
|
452
|
-
/**
|
|
453
|
-
* Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API, mapping a stable external id onto axios's own so `update` can swap a handler in place.
|
|
454
|
-
*/
|
|
455
|
-
function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
|
|
456
|
-
const ids = new Map<number, number>()
|
|
457
|
-
let counter = 0
|
|
458
|
-
return {
|
|
459
|
-
use(fn) {
|
|
460
|
-
const id = ++counter
|
|
461
|
-
ids.set(id, register(fn))
|
|
462
|
-
return id
|
|
463
|
-
},
|
|
464
|
-
eject(id) {
|
|
465
|
-
const nativeId = ids.get(id)
|
|
466
|
-
if (nativeId === undefined) return
|
|
467
|
-
ejectNative(nativeId)
|
|
468
|
-
ids.delete(id)
|
|
469
|
-
},
|
|
470
|
-
update(id, fn) {
|
|
471
|
-
const nativeId = ids.get(id)
|
|
472
|
-
if (nativeId !== undefined) ejectNative(nativeId)
|
|
473
|
-
ids.set(id, register(fn))
|
|
474
|
-
},
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
383
|
/**
|
|
479
384
|
* The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters.
|
|
480
385
|
*/
|
|
@@ -500,6 +405,135 @@ function resolveContentType(contentType: ContentType | undefined): { request?: s
|
|
|
500
405
|
return contentType ?? {}
|
|
501
406
|
}
|
|
502
407
|
|
|
408
|
+
/**
|
|
409
|
+
* The per-concern serializers for a call, the per-call serializer winning over the client's and
|
|
410
|
+
* falling back to the defaults.
|
|
411
|
+
*/
|
|
412
|
+
function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) {
|
|
413
|
+
return {
|
|
414
|
+
querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer,
|
|
415
|
+
bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer,
|
|
416
|
+
pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer,
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Resolves everything a call needs before it touches axios: merged headers with the negotiated
|
|
422
|
+
* content type, auth on headers or query, serialized cookies, the validated and serialized body,
|
|
423
|
+
* and the final axios request config with `throwOnError` riding `validateStatus`.
|
|
424
|
+
*/
|
|
425
|
+
async function resolveRequest<TBody, TRequest, TResponse>({
|
|
426
|
+
config,
|
|
427
|
+
requestConfig,
|
|
428
|
+
}: {
|
|
429
|
+
config: ClientConfig
|
|
430
|
+
requestConfig: RequestConfig<TBody, TRequest, TResponse>
|
|
431
|
+
}): Promise<{ axiosConfig: AxiosRequestConfig; codecs: Record<string, Codec>; throwOnError: boolean }> {
|
|
432
|
+
const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
|
|
433
|
+
const codecs = { ...config.codecs, ...requestConfig.codecs }
|
|
434
|
+
|
|
435
|
+
const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
|
|
436
|
+
const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
|
|
437
|
+
const requestContentType = requestContentTypeOption ?? headers['Content-Type'] ?? headers['content-type']
|
|
438
|
+
if (responseContentType && headers['Accept'] === undefined && headers['accept'] === undefined) {
|
|
439
|
+
headers['Accept'] = responseContentType
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
443
|
+
|
|
444
|
+
await resolveAuth({
|
|
445
|
+
security: requestConfig.security,
|
|
446
|
+
auth: requestConfig.auth ?? config.auth,
|
|
447
|
+
headers,
|
|
448
|
+
query,
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
if (requestConfig.cookies) {
|
|
452
|
+
const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
|
|
453
|
+
if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
|
|
457
|
+
const requestContentTypeBase = baseContentType(requestContentType)
|
|
458
|
+
const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
|
|
459
|
+
const body = contentCodec?.serialize
|
|
460
|
+
? contentCodec.serialize(validatedBody, requestContentType)
|
|
461
|
+
: bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
|
|
462
|
+
// A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
|
|
463
|
+
if (body instanceof FormData) {
|
|
464
|
+
delete headers['Content-Type']
|
|
465
|
+
delete headers['content-type']
|
|
466
|
+
} else if (requestContentTypeOption) {
|
|
467
|
+
headers['Content-Type'] = requestContentTypeOption
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const pathParams = requestConfig.path ?? {}
|
|
471
|
+
const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
472
|
+
pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
|
|
476
|
+
const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
|
|
477
|
+
|
|
478
|
+
const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
|
|
479
|
+
|
|
480
|
+
const axiosConfig: AxiosRequestConfig = {
|
|
481
|
+
...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, …
|
|
482
|
+
url,
|
|
483
|
+
baseURL: requestConfig.baseURL ?? config.baseURL,
|
|
484
|
+
method: requestConfig.method ?? 'GET',
|
|
485
|
+
headers,
|
|
486
|
+
params: query,
|
|
487
|
+
paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
|
|
488
|
+
data: body,
|
|
489
|
+
transformRequest: (data) => data,
|
|
490
|
+
signal: requestConfig.signal,
|
|
491
|
+
responseType: requestConfig.responseType,
|
|
492
|
+
validateStatus,
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Only the fetch adapter exposes a streaming `response.data` (a ReadableStream) in the browser.
|
|
496
|
+
// The default XHR adapter buffers the whole body. Default streams to it, but respect an explicit adapter.
|
|
497
|
+
if (requestConfig.responseType === 'stream' && !axiosConfig.adapter) {
|
|
498
|
+
axiosConfig.adapter = 'fetch'
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return { axiosConfig, codecs, throwOnError }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Turns an axios response into the call result: decodes the body through the matching codec and
|
|
506
|
+
* validates the success or error body. A thrown axios error never reaches this, so `error` here is
|
|
507
|
+
* always the body of a non-2xx response that `validateStatus` let through.
|
|
508
|
+
*/
|
|
509
|
+
async function settleResponse<TRequest, TResponse>({
|
|
510
|
+
response,
|
|
511
|
+
codecs,
|
|
512
|
+
validator,
|
|
513
|
+
}: {
|
|
514
|
+
response: AxiosResponse
|
|
515
|
+
codecs: Record<string, Codec>
|
|
516
|
+
validator: { response?: Validator; error?: Validator } | undefined
|
|
517
|
+
}): Promise<CallResult<TRequest, TResponse>> {
|
|
518
|
+
const isSuccess = response.status >= 200 && response.status < 300
|
|
519
|
+
const contentType = getResponseContentType(response.headers as Record<string, unknown>)
|
|
520
|
+
let decoded: unknown = response.data
|
|
521
|
+
if (contentType) {
|
|
522
|
+
const codec = codecs[contentType]
|
|
523
|
+
if (codec?.deserialize) decoded = await codec.deserialize(response.data, contentType)
|
|
524
|
+
}
|
|
525
|
+
const data = isSuccess ? await runValidator(validator?.response, decoded) : undefined
|
|
526
|
+
const error = isSuccess ? undefined : await runValidator(validator?.error, decoded)
|
|
527
|
+
return {
|
|
528
|
+
status: response.status,
|
|
529
|
+
data,
|
|
530
|
+
error,
|
|
531
|
+
contentType,
|
|
532
|
+
request: response.config as TRequest,
|
|
533
|
+
response: response as TResponse,
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
503
537
|
/**
|
|
504
538
|
* Builds the shared client core bound to an axios instance (defaulting to `axios.create()`), with `throwOnError` riding axios's `validateStatus`.
|
|
505
539
|
*/
|
|
@@ -530,96 +564,11 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
530
564
|
|
|
531
565
|
const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
|
|
532
566
|
const activeInstance = requestConfig.transport ?? config.transport ?? instance
|
|
533
|
-
const
|
|
534
|
-
const bodySerializer = requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer
|
|
535
|
-
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
536
|
-
const codecs = { ...config.codecs, ...requestConfig.codecs }
|
|
537
|
-
|
|
538
|
-
const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
|
|
539
|
-
const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
|
|
540
|
-
const requestContentType = requestContentTypeOption ?? headers['Content-Type'] ?? headers['content-type']
|
|
541
|
-
if (responseContentType && headers['Accept'] === undefined && headers['accept'] === undefined) {
|
|
542
|
-
headers['Accept'] = responseContentType
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
546
|
-
|
|
547
|
-
await resolveAuth({
|
|
548
|
-
security: requestConfig.security,
|
|
549
|
-
auth: requestConfig.auth ?? config.auth,
|
|
550
|
-
headers,
|
|
551
|
-
query,
|
|
552
|
-
})
|
|
553
|
-
|
|
554
|
-
if (requestConfig.cookies) {
|
|
555
|
-
const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
|
|
556
|
-
if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
|
|
560
|
-
const requestContentTypeBase = baseContentType(requestContentType)
|
|
561
|
-
const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
|
|
562
|
-
const body = contentCodec?.serialize
|
|
563
|
-
? contentCodec.serialize(validatedBody, requestContentType)
|
|
564
|
-
: bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
|
|
565
|
-
// A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
|
|
566
|
-
if (body instanceof FormData) {
|
|
567
|
-
delete headers['Content-Type']
|
|
568
|
-
delete headers['content-type']
|
|
569
|
-
} else if (requestContentTypeOption) {
|
|
570
|
-
headers['Content-Type'] = requestContentTypeOption
|
|
571
|
-
}
|
|
572
|
-
const pathParams = requestConfig.path ?? {}
|
|
573
|
-
const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
574
|
-
pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
|
|
575
|
-
)
|
|
576
|
-
const baseURL = [config.baseURL, requestConfig.baseURL].filter(Boolean).join('') || undefined
|
|
577
|
-
|
|
578
|
-
const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
|
|
579
|
-
const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
|
|
580
|
-
|
|
581
|
-
const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
|
|
582
|
-
|
|
583
|
-
const axiosConfig: AxiosRequestConfig = {
|
|
584
|
-
...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, …
|
|
585
|
-
url,
|
|
586
|
-
baseURL,
|
|
587
|
-
method: requestConfig.method ?? 'GET',
|
|
588
|
-
headers,
|
|
589
|
-
params: query,
|
|
590
|
-
paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
|
|
591
|
-
data: body,
|
|
592
|
-
transformRequest: (data) => data,
|
|
593
|
-
signal: requestConfig.signal,
|
|
594
|
-
responseType: requestConfig.responseType,
|
|
595
|
-
validateStatus,
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Only the fetch adapter exposes a streaming `response.data` (a ReadableStream) in the browser;
|
|
599
|
-
// the default XHR adapter buffers the whole body. Default streams to it, but respect an explicit adapter.
|
|
600
|
-
if (requestConfig.responseType === 'stream' && !axiosConfig.adapter) {
|
|
601
|
-
axiosConfig.adapter = 'fetch'
|
|
602
|
-
}
|
|
567
|
+
const { axiosConfig, codecs, throwOnError } = await resolveRequest({ config, requestConfig })
|
|
603
568
|
|
|
604
569
|
try {
|
|
605
570
|
const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
|
|
606
|
-
|
|
607
|
-
const contentType = getResponseContentType(response.headers as Record<string, unknown>)
|
|
608
|
-
let decoded: unknown = response.data
|
|
609
|
-
if (contentType) {
|
|
610
|
-
const codec = codecs[contentType]
|
|
611
|
-
if (codec?.deserialize) decoded = await codec.deserialize(response.data, contentType)
|
|
612
|
-
}
|
|
613
|
-
const data = isSuccess ? await runValidator(requestConfig.validator?.response, decoded) : undefined
|
|
614
|
-
const error = isSuccess ? undefined : await runValidator(requestConfig.validator?.error, decoded)
|
|
615
|
-
return {
|
|
616
|
-
status: response.status,
|
|
617
|
-
data,
|
|
618
|
-
error,
|
|
619
|
-
contentType,
|
|
620
|
-
request: response.config as TRequest,
|
|
621
|
-
response: response as TResponse,
|
|
622
|
-
}
|
|
571
|
+
return await settleResponse({ response, codecs, validator: requestConfig.validator })
|
|
623
572
|
} catch (error) {
|
|
624
573
|
const axiosError = error as AxiosError
|
|
625
574
|
if (throwOnError && axiosError.response) {
|
|
@@ -642,11 +591,10 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
642
591
|
return config
|
|
643
592
|
}
|
|
644
593
|
client.getUrl = (requestConfig) => {
|
|
645
|
-
const querySerializer =
|
|
646
|
-
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
594
|
+
const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
|
|
647
595
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
648
596
|
return serializeUrl({
|
|
649
|
-
parts: [
|
|
597
|
+
parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
|
|
650
598
|
pathParams: requestConfig.path ?? {},
|
|
651
599
|
search: querySerializer(query, requestConfig.styles?.query),
|
|
652
600
|
pathSerializer,
|
|
@@ -659,6 +607,108 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
659
607
|
return client
|
|
660
608
|
}
|
|
661
609
|
|
|
610
|
+
/**
|
|
611
|
+
* One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
|
|
612
|
+
*/
|
|
613
|
+
export type ServerSentEvent<TData = unknown> = {
|
|
614
|
+
data: TData
|
|
615
|
+
event?: string
|
|
616
|
+
id?: string
|
|
617
|
+
retry?: number
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
|
|
621
|
+
if (!('getReader' in stream)) {
|
|
622
|
+
yield* stream
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const reader = stream.getReader()
|
|
627
|
+
try {
|
|
628
|
+
while (true) {
|
|
629
|
+
const { done, value } = await reader.read()
|
|
630
|
+
if (done) return
|
|
631
|
+
yield value
|
|
632
|
+
}
|
|
633
|
+
} finally {
|
|
634
|
+
await reader.cancel().catch(() => {})
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
|
|
639
|
+
const data: Array<string> = []
|
|
640
|
+
const event: ServerSentEvent<TData> = { data: undefined as TData }
|
|
641
|
+
let seen = false
|
|
642
|
+
|
|
643
|
+
for (const line of raw.split('\n')) {
|
|
644
|
+
if (!line || line.startsWith(':')) continue
|
|
645
|
+
seen = true
|
|
646
|
+
const index = line.indexOf(':')
|
|
647
|
+
const field = index === -1 ? line : line.slice(0, index)
|
|
648
|
+
const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
|
|
649
|
+
if (field === 'data') data.push(value)
|
|
650
|
+
else if (field === 'event') event.event = value
|
|
651
|
+
else if (field === 'id') event.id = value
|
|
652
|
+
else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (!seen) return undefined
|
|
656
|
+
|
|
657
|
+
if (data.length) {
|
|
658
|
+
const joined = data.join('\n')
|
|
659
|
+
try {
|
|
660
|
+
event.data = JSON.parse(joined) as TData
|
|
661
|
+
} catch {
|
|
662
|
+
event.data = joined as TData
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return event
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
|
|
670
|
+
*/
|
|
671
|
+
export async function* parseEventStream<TData = unknown>(
|
|
672
|
+
stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
673
|
+
): AsyncGenerator<ServerSentEvent<TData>> {
|
|
674
|
+
const decoder = new TextDecoder()
|
|
675
|
+
const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
|
|
676
|
+
let buffer = ''
|
|
677
|
+
|
|
678
|
+
for await (const chunk of readBytes(stream)) {
|
|
679
|
+
const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
|
|
680
|
+
buffer = blocks.pop() ?? ''
|
|
681
|
+
for (const block of blocks) {
|
|
682
|
+
const event = parseEvent<TData>(block)
|
|
683
|
+
if (event) yield event
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
|
|
688
|
+
if (event) yield event
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* The resolved shape returned by a generated `text/event-stream` operation: the typed event
|
|
693
|
+
* `stream` plus the native `response`.
|
|
694
|
+
*/
|
|
695
|
+
export type EventStreamResult<TData = unknown, TResponse = AxiosResponse> = {
|
|
696
|
+
stream: AsyncGenerator<ServerSentEvent<TData>>
|
|
697
|
+
response: TResponse
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
|
|
702
|
+
* the parsed events as a typed async iterator. Generated SSE operations call this.
|
|
703
|
+
*/
|
|
704
|
+
export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: AxiosResponse }>): Promise<EventStreamResult<TData>> {
|
|
705
|
+
const { data, response } = await result
|
|
706
|
+
return {
|
|
707
|
+
response,
|
|
708
|
+
stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
662
712
|
export const client = createClientCore()
|
|
663
713
|
|
|
664
714
|
export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)
|