@kubb/plugin-axios 5.0.0-beta.80 → 5.0.0-beta.84

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-axios",
3
- "version": "5.0.0-beta.80",
3
+ "version": "5.0.0-beta.84",
4
4
  "description": "Generate a slim, type-safe HTTP client with Kubb based on axios.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -49,22 +49,20 @@
49
49
  "registry": "https://registry.npmjs.org/"
50
50
  },
51
51
  "dependencies": {
52
- "@kubb/ast": "5.0.0-beta.79",
53
- "@kubb/core": "5.0.0-beta.79",
54
- "@kubb/renderer-jsx": "5.0.0-beta.79",
55
- "@kubb/plugin-ts": "5.0.0-beta.80",
56
- "@kubb/plugin-zod": "5.0.0-beta.80"
52
+ "@kubb/plugin-ts": "5.0.0-beta.84",
53
+ "@kubb/plugin-zod": "5.0.0-beta.84"
57
54
  },
58
55
  "devDependencies": {
59
56
  "axios": "^1.18.1",
57
+ "kubb": "5.0.0-beta.84",
60
58
  "typescript": "^6.0.3",
61
59
  "@internals/client": "0.0.0",
62
60
  "@internals/shared": "0.0.0",
63
61
  "@internals/utils": "0.0.0"
64
62
  },
65
63
  "peerDependencies": {
66
- "@kubb/renderer-jsx": "5.0.0-beta.79",
67
- "axios": "^1.7.2"
64
+ "axios": "^1.7.2",
65
+ "kubb": "5.0.0-beta.84"
68
66
  },
69
67
  "peerDependenciesMeta": {
70
68
  "axios": {
@@ -8,10 +8,10 @@ import {
8
8
  type SecurityDocument,
9
9
  } from '@internals/client'
10
10
  import { isEventStream, operationFileEntry } from '@internals/shared'
11
- import { ast, defineGenerator } from '@kubb/core'
11
+ import { ast, defineGenerator } from 'kubb/kit'
12
12
  import { pluginTsName } from '@kubb/plugin-ts'
13
13
  import { pluginZodName } from '@kubb/plugin-zod'
14
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
14
+ import { File, jsxRenderer } from 'kubb/jsx'
15
15
  import type { PluginAxios } from '../types.ts'
16
16
 
17
17
  /**
package/src/plugin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path'
2
2
  import { createSdkGenerator, defaultMacros, isValidatorEnabled, resolverClient } from '@internals/client'
3
3
  import { createGroupConfig } from '@internals/shared'
4
- import { definePlugin } from '@kubb/core'
4
+ import { definePlugin } from 'kubb/kit'
5
5
  import { pluginTsName } from '@kubb/plugin-ts'
6
6
  import { pluginZodName } from '@kubb/plugin-zod'
7
7
  import { clientGenerator } from './generators/clientGenerator.tsx'
@@ -22,7 +22,7 @@ export const pluginAxiosName = 'plugin-axios' satisfies PluginAxios['name']
22
22
  *
23
23
  * @example
24
24
  * ```ts
25
- * import { defineConfig } from 'kubb'
25
+ * import { defineConfig } from 'kubb/config'
26
26
  * import { pluginTs } from '@kubb/plugin-ts'
27
27
  * import { pluginAxios } from '@kubb/plugin-axios'
28
28
  *
@@ -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: baseURL ? `client.setConfig({ baseURL: ${JSON.stringify(baseURL)} })` : undefined,
95
+ footer: baseURLExpression ? `client.setConfig({ baseURL: ${baseURLExpression} })` : undefined,
95
96
  })
96
97
 
97
98
  ctx.injectFile({
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginFactoryOptions } from '@kubb/core'
1
+ import type { PluginFactoryOptions } from 'kubb/kit'
2
2
  import type { Options, ResolvedOptions, ResolverClient } from '@internals/client'
3
3
 
4
4
  export type { Options, ResolvedOptions, ResolverClient } from '@internals/client'
@@ -1,6 +1,6 @@
1
1
  import axios from 'axios'
2
2
  import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
3
- import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, serializeCookies } from './serializers'
3
+ import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, isDefaultJsonBody, serializeCookies } from './serializers'
4
4
  import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers'
5
5
  import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema.ts'
6
6
 
@@ -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
@@ -393,6 +291,15 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
393
291
  return Object.assign({}, ...sources.map(serializeHeaders))
394
292
  }
395
293
 
294
+ function getHeader(headers: Record<string, string>, name: string): string | undefined {
295
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())
296
+ return key ? headers[key] : undefined
297
+ }
298
+
299
+ function hasHeader(headers: Record<string, string>, name: string): boolean {
300
+ return Object.keys(headers).some((k) => k.toLowerCase() === name.toLowerCase())
301
+ }
302
+
396
303
  /**
397
304
  * Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, backing `getUrl`.
398
305
  */
@@ -416,6 +323,32 @@ function serializeUrl({
416
323
  return path + (search ? `?${search}` : '')
417
324
  }
418
325
 
326
+ /**
327
+ * 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.
328
+ */
329
+ function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
330
+ const ids = new Map<number, number>()
331
+ let counter = 0
332
+ return {
333
+ use(fn) {
334
+ const id = ++counter
335
+ ids.set(id, register(fn))
336
+ return id
337
+ },
338
+ eject(id) {
339
+ const nativeId = ids.get(id)
340
+ if (nativeId === undefined) return
341
+ ejectNative(nativeId)
342
+ ids.delete(id)
343
+ },
344
+ update(id, fn) {
345
+ const nativeId = ids.get(id)
346
+ if (nativeId !== undefined) ejectNative(nativeId)
347
+ ids.set(id, register(fn))
348
+ },
349
+ }
350
+ }
351
+
419
352
  /**
420
353
  * Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
421
354
  */
@@ -434,10 +367,14 @@ export async function resolveAuth(params: {
434
367
 
435
368
  if (scheme.type === 'apiKey') {
436
369
  const name = scheme.name ?? 'Authorization'
437
- if (scheme.in === 'query') query[name] = token
438
- else if (scheme.in === 'cookie') headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
439
- else headers[name] = token
440
- } else {
370
+ if (scheme.in === 'query') {
371
+ if (query[name] === undefined) query[name] = token
372
+ } else if (scheme.in === 'cookie') {
373
+ headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
374
+ } else if (!hasHeader(headers, name)) {
375
+ headers[name] = token
376
+ }
377
+ } else if (!hasHeader(headers, 'Authorization')) {
441
378
  headers.Authorization = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}`
442
379
  }
443
380
  return
@@ -449,32 +386,6 @@ async function runValidator<T>(validator: Validator<T> | undefined, value: T): P
449
386
  return validateStandardSchema(validator, value)
450
387
  }
451
388
 
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
389
  /**
479
390
  * The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters.
480
391
  */
@@ -500,6 +411,139 @@ function resolveContentType(contentType: ContentType | undefined): { request?: s
500
411
  return contentType ?? {}
501
412
  }
502
413
 
414
+ /**
415
+ * The per-concern serializers for a call, the per-call serializer winning over the client's and
416
+ * falling back to the defaults.
417
+ */
418
+ function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) {
419
+ return {
420
+ querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer,
421
+ bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer,
422
+ pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer,
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Resolves everything a call needs before it touches axios: merged headers with the negotiated
428
+ * content type, auth on headers or query, serialized cookies, the validated and serialized body,
429
+ * and the final axios request config with `throwOnError` riding `validateStatus`.
430
+ */
431
+ async function resolveRequest<TBody, TRequest, TResponse>({
432
+ config,
433
+ requestConfig,
434
+ }: {
435
+ config: ClientConfig
436
+ requestConfig: RequestConfig<TBody, TRequest, TResponse>
437
+ }): Promise<{ axiosConfig: AxiosRequestConfig; codecs: Record<string, Codec>; throwOnError: boolean }> {
438
+ const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
439
+ const codecs = { ...config.codecs, ...requestConfig.codecs }
440
+
441
+ const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
442
+ const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
443
+ const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
444
+ if (responseContentType && !hasHeader(headers, 'accept')) {
445
+ headers['Accept'] = responseContentType
446
+ }
447
+
448
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
449
+
450
+ await resolveAuth({
451
+ security: requestConfig.security,
452
+ auth: requestConfig.auth ?? config.auth,
453
+ headers,
454
+ query,
455
+ })
456
+
457
+ if (requestConfig.cookies) {
458
+ const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
459
+ if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
460
+ }
461
+
462
+ const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
463
+ const requestContentTypeBase = baseContentType(requestContentType)
464
+ const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
465
+ const usesDefaultBodySerializer = !contentCodec?.serialize && bodySerializer === defaultBodySerializer
466
+ const body = contentCodec?.serialize
467
+ ? contentCodec.serialize(validatedBody, requestContentType)
468
+ : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
469
+ // A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
470
+ if (body instanceof FormData) {
471
+ for (const key of Object.keys(headers)) {
472
+ if (key.toLowerCase() === 'content-type') delete headers[key]
473
+ }
474
+ } else if (requestContentTypeOption) {
475
+ headers['Content-Type'] = requestContentTypeOption
476
+ } else if (usesDefaultBodySerializer && isDefaultJsonBody(validatedBody) && !hasHeader(headers, 'content-type')) {
477
+ headers['Content-Type'] = 'application/json'
478
+ }
479
+
480
+ const pathParams = requestConfig.path ?? {}
481
+ const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
482
+ pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
483
+ )
484
+
485
+ const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
486
+ const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
487
+
488
+ const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
489
+
490
+ const axiosConfig: AxiosRequestConfig = {
491
+ ...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, …
492
+ url,
493
+ baseURL: requestConfig.baseURL ?? config.baseURL,
494
+ method: requestConfig.method ?? 'GET',
495
+ headers,
496
+ params: query,
497
+ paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
498
+ data: body,
499
+ transformRequest: (data) => data,
500
+ signal: requestConfig.signal,
501
+ responseType: requestConfig.responseType,
502
+ validateStatus,
503
+ }
504
+
505
+ // Only the fetch adapter exposes a streaming `response.data` (a ReadableStream) in the browser.
506
+ // The default XHR adapter buffers the whole body. Default streams to it, but respect an explicit adapter.
507
+ if (requestConfig.responseType === 'stream' && !axiosConfig.adapter) {
508
+ axiosConfig.adapter = 'fetch'
509
+ }
510
+
511
+ return { axiosConfig, codecs, throwOnError }
512
+ }
513
+
514
+ /**
515
+ * Turns an axios response into the call result: decodes the body through the matching codec and
516
+ * validates the success or error body. A thrown axios error never reaches this, so `error` here is
517
+ * always the body of a non-2xx response that `validateStatus` let through.
518
+ */
519
+ async function settleResponse<TRequest, TResponse>({
520
+ response,
521
+ codecs,
522
+ validator,
523
+ }: {
524
+ response: AxiosResponse
525
+ codecs: Record<string, Codec>
526
+ validator: { response?: Validator; error?: Validator } | undefined
527
+ }): Promise<CallResult<TRequest, TResponse>> {
528
+ const isSuccess = response.status >= 200 && response.status < 300
529
+ const contentType = getResponseContentType(response.headers as Record<string, unknown>)
530
+ let decoded: unknown = response.data
531
+ if (contentType) {
532
+ const codec = codecs[contentType]
533
+ if (codec?.deserialize) decoded = await codec.deserialize(response.data, contentType)
534
+ }
535
+ const data = isSuccess ? await runValidator(validator?.response, decoded) : undefined
536
+ const error = isSuccess ? undefined : await runValidator(validator?.error, decoded)
537
+ return {
538
+ status: response.status,
539
+ data,
540
+ error,
541
+ contentType,
542
+ request: response.config as TRequest,
543
+ response: response as TResponse,
544
+ }
545
+ }
546
+
503
547
  /**
504
548
  * Builds the shared client core bound to an axios instance (defaulting to `axios.create()`), with `throwOnError` riding axios's `validateStatus`.
505
549
  */
@@ -530,96 +574,11 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
530
574
 
531
575
  const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
532
576
  const activeInstance = requestConfig.transport ?? config.transport ?? instance
533
- const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
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
- }
577
+ const { axiosConfig, codecs, throwOnError } = await resolveRequest({ config, requestConfig })
603
578
 
604
579
  try {
605
580
  const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
606
- const isSuccess = response.status >= 200 && response.status < 300
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
- }
581
+ return await settleResponse({ response, codecs, validator: requestConfig.validator })
623
582
  } catch (error) {
624
583
  const axiosError = error as AxiosError
625
584
  if (throwOnError && axiosError.response) {
@@ -642,11 +601,10 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
642
601
  return config
643
602
  }
644
603
  client.getUrl = (requestConfig) => {
645
- const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
646
- const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
604
+ const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
647
605
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
648
606
  return serializeUrl({
649
- parts: [config.baseURL, requestConfig.baseURL, requestConfig.url],
607
+ parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
650
608
  pathParams: requestConfig.path ?? {},
651
609
  search: querySerializer(query, requestConfig.styles?.query),
652
610
  pathSerializer,
@@ -659,6 +617,108 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
659
617
  return client
660
618
  }
661
619
 
620
+ /**
621
+ * One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
622
+ */
623
+ export type ServerSentEvent<TData = unknown> = {
624
+ data: TData
625
+ event?: string
626
+ id?: string
627
+ retry?: number
628
+ }
629
+
630
+ async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
631
+ if (!('getReader' in stream)) {
632
+ yield* stream
633
+ return
634
+ }
635
+
636
+ const reader = stream.getReader()
637
+ try {
638
+ while (true) {
639
+ const { done, value } = await reader.read()
640
+ if (done) return
641
+ yield value
642
+ }
643
+ } finally {
644
+ await reader.cancel().catch(() => {})
645
+ }
646
+ }
647
+
648
+ function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
649
+ const data: Array<string> = []
650
+ const event: ServerSentEvent<TData> = { data: undefined as TData }
651
+ let seen = false
652
+
653
+ for (const line of raw.split('\n')) {
654
+ if (!line || line.startsWith(':')) continue
655
+ seen = true
656
+ const index = line.indexOf(':')
657
+ const field = index === -1 ? line : line.slice(0, index)
658
+ const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
659
+ if (field === 'data') data.push(value)
660
+ else if (field === 'event') event.event = value
661
+ else if (field === 'id') event.id = value
662
+ else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
663
+ }
664
+
665
+ if (!seen) return undefined
666
+
667
+ if (data.length) {
668
+ const joined = data.join('\n')
669
+ try {
670
+ event.data = JSON.parse(joined) as TData
671
+ } catch {
672
+ event.data = joined as TData
673
+ }
674
+ }
675
+ return event
676
+ }
677
+
678
+ /**
679
+ * Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
680
+ */
681
+ export async function* parseEventStream<TData = unknown>(
682
+ stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
683
+ ): AsyncGenerator<ServerSentEvent<TData>> {
684
+ const decoder = new TextDecoder()
685
+ const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
686
+ let buffer = ''
687
+
688
+ for await (const chunk of readBytes(stream)) {
689
+ const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
690
+ buffer = blocks.pop() ?? ''
691
+ for (const block of blocks) {
692
+ const event = parseEvent<TData>(block)
693
+ if (event) yield event
694
+ }
695
+ }
696
+
697
+ const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
698
+ if (event) yield event
699
+ }
700
+
701
+ /**
702
+ * The resolved shape returned by a generated `text/event-stream` operation: the typed event
703
+ * `stream` plus the native `response`.
704
+ */
705
+ export type EventStreamResult<TData = unknown, TResponse = AxiosResponse> = {
706
+ stream: AsyncGenerator<ServerSentEvent<TData>>
707
+ response: TResponse
708
+ }
709
+
710
+ /**
711
+ * Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
712
+ * the parsed events as a typed async iterator. Generated SSE operations call this.
713
+ */
714
+ export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: AxiosResponse }>): Promise<EventStreamResult<TData>> {
715
+ const { data, response } = await result
716
+ return {
717
+ response,
718
+ stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
719
+ }
720
+ }
721
+
662
722
  export const client = createClientCore()
663
723
 
664
724
  export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)
@@ -122,6 +122,10 @@ function isFormBody(body: unknown): body is BodyInit {
122
122
  )
123
123
  }
124
124
 
125
+ export function isDefaultJsonBody(body: unknown): boolean {
126
+ return body !== undefined && body !== null && !isFormBody(body)
127
+ }
128
+
125
129
  function appendFormDataValue({ formData, key, value, contentType }: { formData: FormData; key: string; value: unknown; contentType?: string }): void {
126
130
  if (value === undefined || value === null) return
127
131
  if (value instanceof Blob) formData.append(key, value)