@kubb/plugin-fetch 5.4.0-canary.20260910T110004 → 5.4.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.
Files changed (2) hide show
  1. package/package.json +9 -5
  2. package/templates/fetch.ts +67 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-fetch",
3
- "version": "5.4.0-canary.20260910T110004",
3
+ "version": "5.4.0",
4
4
  "description": "Generate a slim, type-safe HTTP client with Kubb based on the native Fetch api.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -29,6 +29,10 @@
29
29
  {
30
30
  "type": "opencollective",
31
31
  "url": "https://opencollective.com/kubb"
32
+ },
33
+ {
34
+ "type": "custom",
35
+ "url": "https://kubb.dev/sponsors"
32
36
  }
33
37
  ],
34
38
  "files": [
@@ -61,12 +65,12 @@
61
65
  "registry": "https://registry.npmjs.org/"
62
66
  },
63
67
  "dependencies": {
64
- "@kubb/plugin-ts": "^5.1.0-canary.20260910T110004",
65
- "@kubb/plugin-zod": "^5.2.0-canary.20260910T110004"
68
+ "@kubb/plugin-ts": "^5.0.2",
69
+ "@kubb/plugin-zod": "^5.1.5"
66
70
  },
67
71
  "devDependencies": {
68
- "@internals/client": "0.1.0-canary.20260910T110004",
69
- "@internals/shared": "0.1.0-canary.20260910T110004",
72
+ "@internals/client": "0.0.0",
73
+ "@internals/shared": "0.0.0",
70
74
  "kubb": "^5.2.0",
71
75
  "typescript": "^6.0.3"
72
76
  },
@@ -1,6 +1,6 @@
1
1
  import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, isDefaultJsonBody, serializeCookies } from './serializers'
2
2
  import type { HeadersInit, PathParamStyle, PathSerializer, RequestBody, Serializers, Styles } from './serializers'
3
- import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema'
3
+ import { ParseError, type StandardSchemaValidator, validateStandardSchema } from './standardSchema'
4
4
 
5
5
  /**
6
6
  * HTTP status codes treated as a success, everything else is an error.
@@ -170,6 +170,25 @@ export type ContentType = string | { request?: string; response?: string }
170
170
  */
171
171
  export type Validator<T = unknown> = StandardSchemaValidator<T>
172
172
 
173
+ /**
174
+ * The failing body and the call it came from, handed to `onValidationError` alongside the `ParseError`.
175
+ * `direction` says which slot rejected it: the request body, the success body, or the error body.
176
+ */
177
+ export type ValidationErrorContext = {
178
+ value: unknown
179
+ direction: 'request' | 'response' | 'error'
180
+ method?: string
181
+ url?: string
182
+ status?: number
183
+ }
184
+
185
+ /**
186
+ * Decides what a failed validation does. Returning nothing rethrows the `ParseError`; returning
187
+ * `{ value }` resolves the call with that value instead, so a drifted body can be reported and still
188
+ * delivered. The box keeps an explicit `{ value: undefined }` substitution distinct from declining.
189
+ */
190
+ export type ValidationErrorHandler = (error: ParseError, context: ValidationErrorContext) => { value: unknown } | void | Promise<{ value: unknown } | void>
191
+
173
192
  /**
174
193
  * A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver.
175
194
  */
@@ -220,6 +239,7 @@ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Respo
220
239
  serializer?: Serializers
221
240
  codecs?: Record<string, Codec>
222
241
  validator?: { request?: Validator; response?: Validator; error?: Validator }
242
+ onValidationError?: ValidationErrorHandler
223
243
  security?: Array<Auth>
224
244
  auth?: AuthResolver
225
245
  }
@@ -249,6 +269,7 @@ export type ClientConfig<TRequest = Request, TResponse = Response> = {
249
269
  transport?: Transport<TRequest, TResponse>
250
270
  serializer?: Serializers
251
271
  codecs?: Record<string, Codec>
272
+ onValidationError?: ValidationErrorHandler
252
273
  auth?: AuthResolver
253
274
  }
254
275
 
@@ -463,9 +484,26 @@ export async function resolveAuth(params: {
463
484
  }
464
485
  }
465
486
 
466
- async function runValidator<T>(validator: Validator<T> | undefined, value: T): Promise<T> {
487
+ async function runValidator<T>({
488
+ validator,
489
+ value,
490
+ context,
491
+ onValidationError,
492
+ }: {
493
+ validator: Validator<T> | undefined
494
+ value: T
495
+ context: Omit<ValidationErrorContext, 'value'>
496
+ onValidationError: ValidationErrorHandler | undefined
497
+ }): Promise<T> {
467
498
  if (!validator) return value
468
- return validateStandardSchema(validator, value)
499
+ try {
500
+ return await validateStandardSchema(validator, value)
501
+ } catch (error) {
502
+ if (!onValidationError || !(error instanceof ParseError)) throw error
503
+ const handled = await onValidationError(error, { ...context, value })
504
+ if (!handled) throw error
505
+ return handled.value as T
506
+ }
469
507
  }
470
508
 
471
509
  /**
@@ -541,7 +579,12 @@ async function resolveRequest<TBody, TRequest, TResponse>({
541
579
  if (cookie) headers['Cookie'] = [headers['Cookie'], cookie].filter(Boolean).join('; ')
542
580
  }
543
581
 
544
- const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
582
+ const validatedBody = await runValidator({
583
+ validator: requestConfig.validator?.request,
584
+ value: requestConfig.body,
585
+ context: { direction: 'request', method: requestConfig.method, url: requestConfig.url },
586
+ onValidationError: requestConfig.onValidationError ?? config.onValidationError,
587
+ })
545
588
  const requestContentTypeBase = baseContentType(requestContentType)
546
589
  const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
547
590
  const usesDefaultBodySerializer = !contentCodec?.serialize && bodySerializer === defaultBodySerializer
@@ -591,15 +634,19 @@ async function resolveRequest<TBody, TRequest, TResponse>({
591
634
  */
592
635
  async function settleResult<TRequest, TResponse>({
593
636
  result,
637
+ request,
594
638
  codecs,
595
639
  throwOnError,
596
640
  validator,
641
+ onValidationError,
597
642
  errorInterceptors,
598
643
  }: {
599
644
  result: TransportResult<unknown, TRequest, TResponse>
645
+ request: ResolvedRequest
600
646
  codecs: Record<string, Codec>
601
647
  throwOnError: boolean
602
648
  validator: { response?: Validator; error?: Validator } | undefined
649
+ onValidationError: ValidationErrorHandler | undefined
603
650
  errorInterceptors: InterceptorStack<ResponseError<unknown, TRequest, TResponse>>
604
651
  }): Promise<CallResult<TRequest, TResponse>> {
605
652
  const isSuccess = result.status >= 200 && result.status < 300
@@ -610,12 +657,24 @@ async function settleResult<TRequest, TResponse>({
610
657
  if (codec?.deserialize) decoded = await codec.deserialize(result.data, contentType)
611
658
  }
612
659
 
660
+ const validationContext = { method: request.method, url: request.url, status: result.status }
661
+
613
662
  if (isSuccess) {
614
- const data = await runValidator(validator?.response, decoded)
663
+ const data = await runValidator({
664
+ validator: validator?.response,
665
+ value: decoded,
666
+ context: { direction: 'response', ...validationContext },
667
+ onValidationError,
668
+ })
615
669
  return { status: result.status, data, error: undefined, contentType, request: result.request, response: result.response }
616
670
  }
617
671
 
618
- const error = await runValidator(validator?.error, decoded)
672
+ const error = await runValidator({
673
+ validator: validator?.error,
674
+ value: decoded,
675
+ context: { direction: 'error', ...validationContext },
676
+ onValidationError,
677
+ })
619
678
  if (throwOnError) {
620
679
  const responseError = new ResponseError({
621
680
  data: error,
@@ -655,9 +714,11 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
655
714
 
656
715
  return settleResult({
657
716
  result,
717
+ request: resolvedRequest,
658
718
  codecs,
659
719
  throwOnError: requestConfig.throwOnError ?? config.throwOnError ?? true,
660
720
  validator: requestConfig.validator,
721
+ onValidationError: requestConfig.onValidationError ?? config.onValidationError,
661
722
  errorInterceptors: interceptors.error,
662
723
  })
663
724
  }) as ClientInstance<TRequest, TResponse>