@kubb/plugin-client 5.10.0-canary.20261105T164114 → 5.10.0

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.
@@ -0,0 +1,694 @@
1
+ import type { NativeRequest, NativeResponse } from './client.ts'
2
+ import {
3
+ applyHeaderStyles,
4
+ baseContentType,
5
+ defaultBodySerializer,
6
+ defaultPathSerializer,
7
+ defaultQuerySerializer,
8
+ isDefaultJsonBody,
9
+ serializeCookies,
10
+ } from './serializers.ts'
11
+ import type { HeadersInit, HttpMethod, PathParamStyle, PathSerializer, RequestBody, Serializers, Styles } from './serializers.ts'
12
+ import { isValidationEnabled, runValidator, type StandardSchemaValidator } from './standardSchema.ts'
13
+ import type { ValidateOption, ValidationErrorHandler } from './standardSchema.ts'
14
+
15
+ /**
16
+ * HTTP status codes treated as a success, everything else is an error.
17
+ */
18
+ export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
19
+
20
+ export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
21
+
22
+ export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
23
+
24
+ /**
25
+ * Converts a response record's string status key to its numeric literal, leaving non-numeric keys like `default` as `number`.
26
+ */
27
+ export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}` ? TNumber : number
28
+
29
+ /**
30
+ * Unwraps content-type variants to keep the bare body on `error`.
31
+ */
32
+ export type DataOf<T> = T extends { contentType: string; data: infer TData } ? TData : T
33
+
34
+ /**
35
+ * Keeps `contentType` beside `data` so switching on it narrows the body.
36
+ */
37
+ export type SuccessVariant<TStatus, TEntry, TRequest, TResponse> = TEntry extends { contentType: string; data: unknown }
38
+ ? TEntry extends { contentType: infer TContentType; data: infer TData }
39
+ ? { status: ToStatusNumber<TStatus>; data: TData; error: undefined; contentType: TContentType; request: TRequest; response: TResponse }
40
+ : never
41
+ : { status: ToStatusNumber<TStatus>; data: TEntry; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
42
+
43
+ /**
44
+ * Narrows `data` or `error` when switching on the numeric status.
45
+ */
46
+ export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
47
+ ? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse>
48
+ : {
49
+ status: ToStatusNumber<TStatus>
50
+ data: undefined
51
+ error: DataOf<TResponses[TStatus]>
52
+ contentType: string | undefined
53
+ request: TRequest
54
+ response: TResponse
55
+ }
56
+
57
+ export type ResultUnion<TResponses, TRequest, TResponse> = {
58
+ [TStatus in keyof TResponses]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
59
+ }[keyof TResponses]
60
+
61
+ /**
62
+ * Selects success statuses without letting untyped error payloads widen `data`.
63
+ */
64
+ export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
65
+ [TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
66
+ }[Extract<keyof TResponses, SuccessStatusCode>]
67
+
68
+ /**
69
+ * Narrows `data` and `error` by status. With `throwOnError`, includes only success variants.
70
+ */
71
+ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = ThrowOnError extends true
72
+ ? [SuccessResultUnion<TResponses, TRequest, TResponse>] extends [never]
73
+ ? {
74
+ status: number
75
+ data: SuccessOf<TResponses>
76
+ error: undefined
77
+ contentType: string | undefined
78
+ request: TRequest
79
+ response: TResponse
80
+ }
81
+ : SuccessResultUnion<TResponses, TRequest, TResponse>
82
+ : [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
83
+ ? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
84
+ : ResultUnion<TResponses, TRequest, TResponse>
85
+
86
+ /**
87
+ * A `RequestResult` promise with an extra `unwrap()` method that resolves to the success body.
88
+ */
89
+ export type Unwrappable<T extends { data: unknown; error: unknown }> = Promise<T> & {
90
+ unwrap: () => Promise<Extract<T, { error: undefined }>['data']>
91
+ }
92
+
93
+ /**
94
+ * Attaches `unwrap()` to a result promise, which rejects with `error` when the result carried one.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * const { data, error } = await getPetById({ path: { petId: 1 } })
99
+ * ```
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const pet = await getPetById({ path: { petId: 1 } }).unwrap()
104
+ * ```
105
+ */
106
+ export function withUnwrap<T extends { data: unknown; error: unknown }>(promise: Promise<T>): Unwrappable<T> {
107
+ const unwrappable = promise as Unwrappable<T>
108
+ unwrappable.unwrap = () =>
109
+ promise.then((result) => {
110
+ if (result.error !== undefined) throw result.error
111
+ return result.data as Extract<T, { error: undefined }>['data']
112
+ })
113
+ return unwrappable
114
+ }
115
+
116
+ /**
117
+ * The shape a generated operation returns with `returnType: 'data'`: the success body when
118
+ * `throwOnError` is true, or the full `RequestResult` when it is false.
119
+ */
120
+ export type UnwrappedResult<TResponses, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = ThrowOnError extends true
121
+ ? RequestResult<TResponses, true, TRequest, TResponse>['data']
122
+ : RequestResult<TResponses, ThrowOnError, TRequest, TResponse>
123
+
124
+ /**
125
+ * Returns the success body when `throwOnError` is true, or the full result when it is false.
126
+ */
127
+ export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
128
+ return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
129
+ }
130
+
131
+ export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
132
+
133
+ export type RequestCredentials = 'omit' | 'same-origin' | 'include'
134
+ export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' | 'formdata'
135
+
136
+ /**
137
+ * Decodes a response body for a codec's media type.
138
+ */
139
+ export type Deserializer<T = unknown> = (raw: unknown, contentType: string) => T | Promise<T>
140
+
141
+ /**
142
+ * Encodes a request body for a codec's media type.
143
+ */
144
+ export type ContentBodySerializer = (body: unknown, contentType?: string) => RequestBody | undefined
145
+
146
+ /**
147
+ * Encodes requests or decodes responses for a media type. Either direction can be omitted.
148
+ */
149
+ export type Codec = {
150
+ serialize?: ContentBodySerializer
151
+ deserialize?: Deserializer
152
+ }
153
+
154
+ /**
155
+ * A string sets the request content type. The object form can also set `Accept`.
156
+ */
157
+ export type ContentType = string | { request?: string; response?: string }
158
+
159
+ /**
160
+ * Validates request bodies and params, success bodies, or non-throwing error bodies with Standard Schema.
161
+ */
162
+ export type Validator<T = unknown> = StandardSchemaValidator<T>
163
+
164
+ export type { ValidateOption, ValidationErrorContext, ValidationErrorHandler } from './standardSchema.ts'
165
+
166
+ /**
167
+ * A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver.
168
+ */
169
+ export type Auth = {
170
+ type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
171
+ scheme?: 'bearer' | 'basic'
172
+ name?: string
173
+ in?: 'header' | 'query' | 'cookie'
174
+ scopes?: Array<string>
175
+ }
176
+
177
+ /**
178
+ * Return `user:password` for basic auth, or `undefined` to skip a scheme.
179
+ */
180
+ export type AuthToken = string | undefined
181
+
182
+ /**
183
+ * Resolves the token for a security scheme, either a static token or a callback called per scheme until one returns a token.
184
+ */
185
+ export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
186
+
187
+ export type RequestConfig<TBody = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = {
188
+ baseURL?: string
189
+ url?: string
190
+ method?: HttpMethod
191
+ path?: unknown
192
+ query?: unknown
193
+ /** Alias of `query`, kept for callers that pass the query under this name. */
194
+ params?: unknown
195
+ cookies?: unknown
196
+ body?: TBody
197
+ headers?: unknown
198
+ styles?: Styles
199
+ signal?: AbortSignal
200
+ credentials?: RequestCredentials
201
+ options?: Record<string, unknown>
202
+ contentType?: ContentType
203
+ responseType?: ResponseType
204
+ throwOnError?: boolean
205
+ client?: ClientInstance<TRequest, TResponse>
206
+ transport?: Transport<TRequest, TResponse>
207
+ serializer?: Serializers
208
+ codecs?: Record<string, Codec>
209
+ validator?: { request?: Validator; path?: Validator; query?: Validator; headers?: Validator; response?: Validator; error?: Validator }
210
+ onValidationError?: ValidationErrorHandler
211
+ validate?: ValidateOption
212
+ security?: Array<Auth>
213
+ auth?: AuthResolver
214
+ }
215
+
216
+ /**
217
+ * Combines client options with the operation's typed request params and body.
218
+ */
219
+ export type Options<TData extends DataShape, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = Omit<
220
+ RequestConfig<unknown, TRequest, TResponse>,
221
+ keyof DataShape | 'url'
222
+ > &
223
+ TData & {
224
+ client?: ClientInstance<TRequest, TResponse>
225
+ throwOnError?: ThrowOnError
226
+ }
227
+
228
+ /**
229
+ * Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`.
230
+ */
231
+ export type ClientConfig<TRequest = NativeRequest, TResponse = NativeResponse> = {
232
+ baseURL?: string
233
+ headers?: HeadersInit
234
+ credentials?: RequestCredentials
235
+ options?: Record<string, unknown>
236
+ throwOnError?: boolean
237
+ transport?: Transport<TRequest, TResponse>
238
+ serializer?: Serializers
239
+ codecs?: Record<string, Codec>
240
+ onValidationError?: ValidationErrorHandler
241
+ validate?: ValidateOption
242
+ auth?: AuthResolver
243
+ }
244
+
245
+ /**
246
+ * The normalized request the transport receives, with all serialization, auth, and header work already done.
247
+ */
248
+ export type ResolvedRequest = {
249
+ url: string
250
+ method: string
251
+ headers: Record<string, string>
252
+ body?: RequestBody
253
+ signal?: AbortSignal
254
+ credentials?: RequestCredentials
255
+ options?: Record<string, unknown>
256
+ responseType?: ResponseType
257
+ }
258
+
259
+ /**
260
+ * What a transport returns: the parsed body plus the native request and response objects.
261
+ */
262
+ export type TransportResult<TData = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = {
263
+ data: TData
264
+ status: number
265
+ statusText: string
266
+ headers: Headers
267
+ contentType?: string
268
+ request: TRequest
269
+ response: TResponse
270
+ }
271
+
272
+ /**
273
+ * The per-plugin send, supplied to `createClientCore` as `defaultTransport`.
274
+ */
275
+ export type Transport<TRequest = NativeRequest, TResponse = NativeResponse> = (
276
+ request: ResolvedRequest,
277
+ ) => Promise<TransportResult<unknown, TRequest, TResponse>>
278
+
279
+ export type CallResult<TRequest = NativeRequest, TResponse = NativeResponse> = {
280
+ status: number
281
+ data: unknown
282
+ error: unknown
283
+ contentType: string | undefined
284
+ request: TRequest
285
+ response: TResponse
286
+ }
287
+
288
+ export type InterceptorFn<T, TContext = never> = (value: T, context?: TContext) => T | Promise<T>
289
+
290
+ /**
291
+ * A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API.
292
+ */
293
+ export type InterceptorStack<T, TContext = never> = {
294
+ use: (fn: InterceptorFn<T, TContext>) => number
295
+ eject: (id: number) => void
296
+ update: (id: number, fn: InterceptorFn<T, TContext>) => void
297
+ run: (value: T, context?: TContext) => Promise<T>
298
+ }
299
+
300
+ export type Interceptors<TRequest = NativeRequest, TResponse = NativeResponse> = {
301
+ request: InterceptorStack<ResolvedRequest, RequestConfig<unknown, TRequest, TResponse>>
302
+ response: InterceptorStack<TransportResult<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>
303
+ error: InterceptorStack<ResponseError<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>
304
+ }
305
+
306
+ export type UrlConfig = Pick<RequestConfig, 'baseURL' | 'url' | 'path' | 'query' | 'params' | 'serializer' | 'styles'>
307
+
308
+ /**
309
+ * A client instance: the callable send plus configuration, interceptors, and an isolated
310
+ * `createClient` factory bound to the same transport.
311
+ */
312
+ export type ClientInstance<TRequest = NativeRequest, TResponse = NativeResponse> = {
313
+ <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
314
+ getConfig: () => ClientConfig<TRequest, TResponse>
315
+ setConfig: (config: ClientConfig<TRequest, TResponse>) => ClientConfig<TRequest, TResponse>
316
+ getUrl: (config: UrlConfig) => string
317
+ interceptors: Interceptors<TRequest, TResponse>
318
+ createClient: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>
319
+ }
320
+
321
+ /**
322
+ * Thrown for a non-2xx response, so a resolved call always means success.
323
+ */
324
+ export class ResponseError<TError = unknown, TRequest = NativeRequest, TResponse = NativeResponse> extends Error {
325
+ data: TError
326
+ status: number
327
+ statusText: string
328
+ contentType: string | undefined
329
+ request: TRequest
330
+ response: TResponse
331
+
332
+ constructor(config: {
333
+ data: TError
334
+ status: number
335
+ statusText: string
336
+ contentType?: string
337
+ request: TRequest
338
+ response: TResponse
339
+ method?: string
340
+ url?: string
341
+ }) {
342
+ // The query and hash are left out: sensitive query parameters or fragments must not reach logs.
343
+ const cleanUrl = config.url?.split(/[?#]/)[0]
344
+ const target = [config.method, cleanUrl].filter(Boolean).join(' ')
345
+ const statusText = config.statusText?.trim()
346
+ super(`${target ? `${target} failed` : 'Request failed'} with status ${config.status}${statusText ? ` ${statusText}` : ''}`)
347
+ this.name = 'ResponseError'
348
+ this.data = config.data
349
+ this.status = config.status
350
+ this.statusText = config.statusText
351
+ this.contentType = config.contentType
352
+ this.request = config.request
353
+ this.response = config.response
354
+ }
355
+
356
+ /**
357
+ * Matches on `name`, not `instanceof`: every generated client bundles its own `ResponseError` class.
358
+ */
359
+ static is(error: unknown): error is ResponseError<unknown, unknown, unknown> {
360
+ return error instanceof Error && error.name === 'ResponseError'
361
+ }
362
+ }
363
+
364
+ export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
365
+
366
+ function headerEntries(headers: object): Array<[string, unknown]> {
367
+ if (Array.isArray(headers)) return headers
368
+ return headers instanceof Headers ? [...headers] : Object.entries(headers)
369
+ }
370
+
371
+ function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
372
+ if (!headers) return {}
373
+ const entries = headerEntries(headers)
374
+ const result: Record<string, string> = {}
375
+ for (const [key, value] of entries) {
376
+ if (value === undefined || value === null) continue
377
+ result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
378
+ }
379
+ return result
380
+ }
381
+
382
+ function collectHeadersCaseInsensitive(...sources: Array<unknown>): Record<string, unknown> {
383
+ const keys = new Map<string, string>()
384
+ const result: Record<string, unknown> = {}
385
+ for (const source of sources) {
386
+ if (!source) continue
387
+ const entries = headerEntries(source)
388
+ for (const [key, value] of entries) {
389
+ if (value === undefined || value === null) continue
390
+ const lowerCase = key.toLowerCase()
391
+ const previous = keys.get(lowerCase)
392
+ if (previous !== undefined) delete result[previous]
393
+ keys.set(lowerCase, key)
394
+ result[key] = value
395
+ }
396
+ }
397
+ return result
398
+ }
399
+
400
+ function getHeader(headers: Record<string, string>, name: string): string | undefined {
401
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())
402
+ return key ? headers[key] : undefined
403
+ }
404
+
405
+ function serializeUrl({
406
+ parts,
407
+ pathParams,
408
+ search,
409
+ pathSerializer = defaultPathSerializer,
410
+ pathStyles,
411
+ }: {
412
+ parts: Array<string | undefined>
413
+ pathParams: Record<string, unknown>
414
+ search: string
415
+ pathSerializer?: PathSerializer
416
+ pathStyles?: Record<string, PathParamStyle>
417
+ }): string {
418
+ const path = parts
419
+ .filter(Boolean)
420
+ .join('')
421
+ .replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
422
+ return path + (search ? `?${search}` : '')
423
+ }
424
+
425
+ /**
426
+ * Creates a transport-agnostic interceptor channel that runs interceptors in registration order.
427
+ */
428
+ export function createInterceptorStack<T, TContext = never>(): InterceptorStack<T, TContext> {
429
+ let entries: Array<{ id: number; fn: InterceptorFn<T, TContext> }> = []
430
+ let counter = 0
431
+ return {
432
+ use(fn) {
433
+ const id = ++counter
434
+ entries.push({ id, fn })
435
+ return id
436
+ },
437
+ eject(id) {
438
+ entries = entries.filter((entry) => entry.id !== id)
439
+ },
440
+ update(id, fn) {
441
+ const entry = entries.find((item) => item.id === id)
442
+ if (entry) entry.fn = fn
443
+ },
444
+ async run(value, context) {
445
+ let result = value
446
+ for (const entry of entries) {
447
+ result = await entry.fn(result, context)
448
+ }
449
+ return result
450
+ },
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
456
+ */
457
+ export async function resolveAuth(params: {
458
+ security: Array<Auth> | undefined
459
+ auth: AuthResolver | undefined
460
+ headers: Record<string, string>
461
+ query: Record<string, unknown>
462
+ }): Promise<void> {
463
+ const { security, auth, headers, query } = params
464
+ if (!security?.length || auth === undefined) return
465
+
466
+ for (const scheme of security) {
467
+ const token = typeof auth === 'function' ? await auth(scheme) : auth
468
+ if (token === undefined) continue
469
+
470
+ if (scheme.type !== 'apiKey') {
471
+ if (getHeader(headers, 'Authorization') === undefined) {
472
+ headers['Authorization'] = scheme.scheme === 'basic' ? `Basic ${btoa(String.fromCharCode(...new TextEncoder().encode(token)))}` : `Bearer ${token}`
473
+ }
474
+ return
475
+ }
476
+
477
+ const name = scheme.name ?? 'Authorization'
478
+ if (scheme.in === 'query') {
479
+ if (query[name] === undefined) query[name] = token
480
+ return
481
+ }
482
+ if (scheme.in === 'cookie') {
483
+ headers['Cookie'] = [headers['Cookie'], `${name}=${token}`].filter(Boolean).join('; ')
484
+ return
485
+ }
486
+ if (getHeader(headers, name) === undefined) headers[name] = token
487
+ return
488
+ }
489
+ }
490
+
491
+ /** A body a transport left as a stream: a web `ReadableStream` or an async iterable such as a Node readable. */
492
+ export function isStreamed(data: unknown): data is ReadableStream<Uint8Array> | AsyncIterable<Uint8Array | string> {
493
+ return data instanceof ReadableStream || (!!data && typeof data === 'object' && Symbol.asyncIterator in data)
494
+ }
495
+
496
+ /** The media type of the `Content-Type` response header, without its parameters. */
497
+ export function getResponseContentType(headers: Headers | Record<string, string> | undefined): string | undefined {
498
+ if (!headers) return undefined
499
+ const value = headers instanceof Headers ? headers.get('Content-Type') : (headers['Content-Type'] ?? headers['content-type'])
500
+ return baseContentType(value)
501
+ }
502
+
503
+ function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) {
504
+ return {
505
+ querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer,
506
+ bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer,
507
+ pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer,
508
+ }
509
+ }
510
+
511
+ async function resolveRequest<TBody, TRequest, TResponse>({
512
+ config,
513
+ requestConfig,
514
+ }: {
515
+ config: ClientConfig<TRequest, TResponse>
516
+ requestConfig: RequestConfig<TBody, TRequest, TResponse>
517
+ }): Promise<{ request: ResolvedRequest; codecs: Record<string, Codec>; validate: ValidateOption | undefined }> {
518
+ const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
519
+ const codecs = { ...config.codecs, ...requestConfig.codecs }
520
+ const onValidationError = requestConfig.onValidationError ?? config.onValidationError
521
+ const validate = requestConfig.validate ?? config.validate
522
+ const validationContext = { direction: 'request', method: requestConfig.method, url: requestConfig.url } as const
523
+
524
+ // Params are encoded before anything reads them: headers before styling, query before auth adds its values.
525
+ const paramValues = {
526
+ path: { ...(requestConfig.path as Record<string, unknown> | undefined) },
527
+ query: { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) },
528
+ headers: collectHeadersCaseInsensitive(config.headers, requestConfig.headers),
529
+ }
530
+ const [validatedPath, validatedQuery, validatedHeaders] = await Promise.all(
531
+ (['path', 'query', 'headers'] as const).map((source) =>
532
+ runValidator({
533
+ validator: isValidationEnabled(validate, 'params') ? requestConfig.validator?.[source] : undefined,
534
+ value: paramValues[source],
535
+ context: { ...validationContext, source },
536
+ onValidationError,
537
+ }),
538
+ ),
539
+ )
540
+
541
+ const headers = serializeHeaders(applyHeaderStyles(validatedHeaders as HeadersInit | undefined, requestConfig.styles?.header))
542
+ const contentType = requestConfig.contentType
543
+ const { request: requestContentTypeOption, response: responseContentType } = typeof contentType === 'string' ? { request: contentType } : (contentType ?? {})
544
+ const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
545
+ if (responseContentType && getHeader(headers, 'accept') === undefined) {
546
+ headers['Accept'] = responseContentType
547
+ }
548
+
549
+ const query: Record<string, unknown> = { ...(validatedQuery as Record<string, unknown> | undefined) }
550
+
551
+ await resolveAuth({
552
+ security: requestConfig.security,
553
+ auth: requestConfig.auth ?? config.auth,
554
+ headers,
555
+ query,
556
+ })
557
+
558
+ if (requestConfig.cookies) {
559
+ const cookie = serializeCookies(requestConfig.cookies as Record<string, unknown>, requestConfig.styles?.cookie)
560
+ if (cookie) headers['Cookie'] = [headers['Cookie'], cookie].filter(Boolean).join('; ')
561
+ }
562
+
563
+ const validatedBody = await runValidator({
564
+ validator: isValidationEnabled(validate, 'request') ? requestConfig.validator?.request : undefined,
565
+ value: requestConfig.body,
566
+ context: { ...validationContext, source: 'body' },
567
+ onValidationError,
568
+ })
569
+ const requestContentTypeBase = baseContentType(requestContentType)
570
+ const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
571
+ const usesDefaultBodySerializer = !contentCodec?.serialize && bodySerializer === defaultBodySerializer
572
+ const body = contentCodec?.serialize
573
+ ? contentCodec.serialize(validatedBody, requestContentType)
574
+ : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
575
+ if (requestContentTypeOption) headers['Content-Type'] = requestContentTypeOption
576
+ if (usesDefaultBodySerializer && isDefaultJsonBody(validatedBody) && getHeader(headers, 'content-type') === undefined) {
577
+ headers['Content-Type'] = 'application/json'
578
+ }
579
+ // The transport must set the multipart boundary, even when a content type was explicitly provided.
580
+ if (body instanceof FormData) {
581
+ for (const key of Object.keys(headers)) {
582
+ if (key.toLowerCase() === 'content-type') delete headers[key]
583
+ }
584
+ }
585
+
586
+ const url = serializeUrl({
587
+ parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
588
+ pathParams: (validatedPath ?? {}) as Record<string, unknown>,
589
+ search: querySerializer(query, requestConfig.styles?.query),
590
+ pathSerializer,
591
+ pathStyles: requestConfig.styles?.path,
592
+ })
593
+
594
+ const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
595
+
596
+ return {
597
+ codecs,
598
+ validate,
599
+ request: {
600
+ url,
601
+ method: (requestConfig.method ?? 'GET').toUpperCase(),
602
+ headers,
603
+ body,
604
+ signal: requestConfig.signal,
605
+ credentials: requestConfig.credentials ?? config.credentials,
606
+ options,
607
+ responseType: requestConfig.responseType,
608
+ },
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Creates an isolated client using the supplied default transport.
614
+ */
615
+ export function createClientCore<TRequest = NativeRequest, TResponse = NativeResponse>(
616
+ options: { defaultTransport: Transport<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
617
+ ): ClientInstance<TRequest, TResponse> {
618
+ const { defaultTransport, ...initialConfig } = options
619
+ let config: ClientConfig<TRequest, TResponse> = { ...initialConfig }
620
+
621
+ const interceptors: Interceptors<TRequest, TResponse> = {
622
+ request: createInterceptorStack<ResolvedRequest, RequestConfig<unknown, TRequest, TResponse>>(),
623
+ response: createInterceptorStack<TransportResult<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>(),
624
+ error: createInterceptorStack<ResponseError<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>(),
625
+ }
626
+
627
+ const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
628
+ const transport = requestConfig.transport ?? config.transport ?? defaultTransport
629
+ const { request, codecs, validate } = await resolveRequest({ config, requestConfig })
630
+
631
+ const resolvedRequest = await interceptors.request.run(request, requestConfig)
632
+ const result = await interceptors.response.run(await transport(resolvedRequest), requestConfig)
633
+
634
+ const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
635
+ const validator = requestConfig.validator
636
+ const onValidationError = requestConfig.onValidationError ?? config.onValidationError
637
+ const isSuccess = result.status >= 200 && result.status < 300
638
+ const direction = isSuccess ? 'response' : 'error'
639
+ const contentType = result.contentType ?? getResponseContentType(result.headers)
640
+ const codec = contentType ? codecs[contentType] : undefined
641
+ const decoded = contentType && codec?.deserialize ? await codec.deserialize(result.data, contentType) : result.data
642
+ // A streamed success body is validated event by event in `toEventStream`, not as a whole. A JSON answer to a streamed call is a whole body.
643
+ const streaming = isSuccess && resolvedRequest.responseType === 'stream' && isStreamed(decoded)
644
+ const data = await runValidator({
645
+ validator: isValidationEnabled(validate, direction) && !streaming ? validator?.[direction] : undefined,
646
+ value: decoded,
647
+ context: { direction, source: 'body', method: resolvedRequest.method, url: resolvedRequest.url, status: result.status },
648
+ onValidationError,
649
+ })
650
+ if (!isSuccess && throwOnError) {
651
+ const responseError = new ResponseError({
652
+ data,
653
+ status: result.status,
654
+ statusText: result.statusText,
655
+ contentType,
656
+ request: result.request,
657
+ response: result.response,
658
+ method: resolvedRequest.method,
659
+ url: resolvedRequest.url,
660
+ })
661
+ await interceptors.error.run(responseError, requestConfig)
662
+ throw responseError
663
+ }
664
+ return {
665
+ status: result.status,
666
+ data: isSuccess ? data : undefined,
667
+ error: isSuccess ? undefined : data,
668
+ contentType,
669
+ request: result.request,
670
+ response: result.response,
671
+ }
672
+ }) as ClientInstance<TRequest, TResponse>
673
+
674
+ client.getConfig = () => config
675
+ client.setConfig = (next) => {
676
+ config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } }
677
+ return config
678
+ }
679
+ client.getUrl = (requestConfig) => {
680
+ const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
681
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
682
+ return serializeUrl({
683
+ parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
684
+ pathParams: (requestConfig.path ?? {}) as Record<string, unknown>,
685
+ search: querySerializer(query, requestConfig.styles?.query),
686
+ pathSerializer,
687
+ pathStyles: requestConfig.styles?.path,
688
+ })
689
+ }
690
+ client.interceptors = interceptors
691
+ client.createClient = (next) => createClientCore({ defaultTransport, ...config, ...next })
692
+
693
+ return client
694
+ }