Skip to content

fix(#2204/#3150/#3519)!: proper error handling for all clients#3814

Open
SukkaW wants to merge 11 commits intohey-api:mainfrom
SukkaW:fix-3150
Open

fix(#2204/#3150/#3519)!: proper error handling for all clients#3814
SukkaW wants to merge 11 commits intohey-api:mainfrom
SukkaW:fix-3150

Conversation

@SukkaW
Copy link
Copy Markdown
Contributor

@SukkaW SukkaW commented Apr 22, 2026

Fixes #2204
Fixes #3150
Fixes #3519

By fixing those issues, the PR has to change the behaviors of how clients handle errors and how error interceptors are invoked, thus should be considered as breaking changes. The PR has tried to minimize any breaking changes.

What this PR does:

  • BREAKING: Currently, throwOnError: false may still throw an error (instead of returning a result object { error, data }), typically when request validation (via schema like zod, valibot, etc.) fails (as seen in Errors from Zod validator does not trigger the error interceptor #3150), or failed to build a request object (as seen in ThrowOnError not working #2204). Now this has changed. Clients now truly respect throwOnError: true and will always "swallow" all errors and return a result object { error, data }.
    • If you use throwOnError: true, this won't affect you. But if you use throwOnError: false, you should be aware that previously thrown errors may now be returned as part of the result object instead, and you should double check how you handle errors.
    • BREAKING: In the result object, request field and response field (if any) may now be undefined. This is because an error may happen during the building of the request or be caused by network issues, in which case there won't be a request or response object, so you should check for the existence of request and response before using them. The TypeScript types have been updated to reflect this change
  • BREAKING: The error interceptor functions now may provide request and/or response parameters as undefined when invoked (as seen in Incorectly typed error interceptor of fetch-client #3519). Same reason as above when we can't obtain a request or response object.
    • If you don't use error interceptors (client.interceptors.error), this won't affect you. But if you do, you should check the request and response parameters in your error interceptor functions for undefined before using them. The TypeScript types have been updated to reflect this change.
  • BREAKING: Currently, client-fetch, client-ky, and client-next do not invoke error interceptors with previous interceptors' returned error objects (but the original error object, discarding previous intermediate transformations). This is also now fixed. The new behavior should now be corrected, but it is a behavior change nonetheless.
    • If you don't use error interceptors (client.interceptors.error), or if you do, but aren't using the above-mentioned clients, you are not affected. But if you do, you should double-check what error object you are expecting.

@mrlubos As usual, I didn't include a changeset in this PR. Feel free to push one as you see fit for the actual CHANGELOG.

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

@SukkaW is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

⚠️ No Changeset found

Latest commit: e728843

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@SukkaW SukkaW changed the title fix(#3150)!: wrap all clients request in the try catch fix(#2204/#3150)!: wrap all clients request in the try catch Apr 22, 2026
@SukkaW SukkaW marked this pull request as ready for review April 22, 2026 19:58
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. bug 🔥 Broken or incorrect behavior. labels Apr 22, 2026
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 22, 2026

TL;DR — Wraps the entire request() lifecycle in every client inside a single try/catch so that errors from any stage — request construction, interceptors, schema validation, network, and response parsing — flow through the error interceptor pipeline and respect throwOnError. This is a breaking change: request and response become potentially undefined in error results and interceptor signatures. The latest commits also fix error interceptor chaining so each interceptor receives the previous interceptor's output, prevent double error-interceptor invocation in the Ky client via an errorInterceptorsInvoked guard, and update example tests to use non-null assertions.

Key changes

  • Unified error handling across all 5 clientsbeforeRequest(), new Request(), request interceptors, fetch(), and response parsing are now all inside a single try block. Previously, only the network call was wrapped, so errors from request building or schema validation could escape unhandled.
  • request and response made optional in error types — The RequestResult error branch and ErrInterceptor signatures now type request and response as potentially undefined, reflecting that errors can occur before either exists. All five clients' types.ts and utils.ts bundles are updated consistently.
  • throwOnError and responseStyle resolved before try block — These options are read from per-request options (with fallback to _config) early, so they remain available in the catch even if beforeRequest() fails.
  • HTTP error responses unified with catch path — Non-ok responses are parsed and then thrown into the shared catch block, eliminating duplicate error-handling logic.
  • Error interceptor chaining fixed — The error interceptor loop now passes finalError (the accumulated result) to each successive interceptor instead of always passing the original error. This ensures interceptors that transform errors form a proper pipeline.
  • Ky client: errorInterceptorsInvoked guard prevents double interceptor invocation — Because parseErrorResponse internally runs error interceptors and may re-throw (when throwOnError is true), the outer catch would invoke interceptors a second time. A boolean flag gates the outer interceptor loop so it only runs for errors that did not already pass through parseErrorResponse.
  • Non-null assertions added to test property accesses — Tests across client-fetch, client-ky, client-ofetch, and example projects (Fastify, NestJS) updated to use ! on result.request and result.response accesses, aligning with the now-optional types.
  • Regenerated all snapshots and examples — 680+ generated files (client.gen.ts, types.gen.ts, utils.gen.ts) updated across test snapshots and examples to reflect the new pattern.

Summary | 702 files | 11 commits | base: mainfix-3150


Unified try/catch around the full request lifecycle

Before: Only the network call (e.g. fetch()) was inside try/catch. Errors from beforeRequest(), new Request(), or request interceptors were unhandled — they bypassed error interceptors and ignored throwOnError.
After: The entire flow — option resolution, request construction, interceptor execution, network call, and response parsing — lives inside a single try block. All errors funnel into one catch that runs the error interceptor pipeline, checks throwOnError, and returns a consistent result shape.

This directly fixes #3150 (Zod validator errors not triggering error interceptors) and #2204 (throwOnError not working for all error types). HTTP error responses (non-ok status) are now parsed and thrown into the same catch block, eliminating the previous duplicate error-handling code path.

throwOnError and responseStyle are captured from user-provided options (with fallback to _config) before the try block, ensuring consistent error behavior even when beforeRequest() throws.

How does the unified catch work?

The catch block handles every possible failure: request building, interceptor errors, network errors (AbortError, timeouts), and HTTP error responses. It runs all registered error interceptors in sequence, then either re-throws (if throwOnError is truthy) or returns the error result object. For responseStyle: 'data', it returns undefined on error.

client-fetch/bundle/client.ts · client-angular/bundle/client.ts · client-ky/bundle/client.ts · client-next/bundle/client.ts · client-ofetch/bundle/client.ts


Optional request and response in error types

Before: Error results and interceptors typed request and response as always defined.
After: Both are optional/undefined-able — request because the error may occur during request construction, response because network errors produce no response.

The RequestResult error branch now types request?: Request (and response?: Response for Ky, Next, oFetch). The ErrInterceptor callback signature changes request: Req to request: Req | undefined and response: Res to response: Res | undefined, with JSDoc comments explaining why each may be absent. Consumers using error interceptors or destructuring error results will need to add null checks.

Tests across client-fetch, client-ky, and client-ofetch were updated to use the non-null assertion operator (!) on result.request and result.response accesses, since these properties are now typed as optional but are known to be defined in the test scenarios. The Fastify and NestJS example tests received the same treatment.

client-fetch/bundle/types.ts · client-angular/bundle/types.ts · client-ky/bundle/types.ts · client-next/bundle/types.ts · client-ofetch/bundle/types.ts


Error interceptor chaining fix

Before: The error interceptor loop always passed the original caught error to every interceptor function, discarding intermediate transformations.
After: Each interceptor receives finalError — the accumulated output from prior interceptors — so the chain works correctly.

The fix (commit e72ab28) corrects a subtle bug in the error interceptor pipeline. Previously, let finalError = error was set, but the loop still called fn(error, ...) instead of fn(finalError, ...). This meant that if an error interceptor transformed the error, the next interceptor in the chain would still receive the original, untransformed error. The fix ensures proper chaining across custom-client and all five bundle client templates.

custom-client/src/client.ts · client-ky/bundle/client.ts


Ky client: errorInterceptorsInvoked guard

Before: When parseErrorResponse ran error interceptors and then re-threw (because throwOnError was true), the outer catch block would run the same interceptors again — double invocation.
After: An errorInterceptorsInvoked boolean flag is set to true before each parseErrorResponse call. The outer catch checks this flag and skips the interceptor loop if they already ran.

This is specific to the Ky client because of its nested try/catch structure — the inner try catches Ky's HTTPError to extract the response, then delegates to parseErrorResponse. If that function throws (for throwOnError), the error bubbles to the outer catch where interceptors would normally run. The flag breaks this cycle. The other clients (fetch, angular, next, ofetch) do not need this guard because their control flow does not create the same re-throw path.

client-ky/bundle/client.ts · client-ky/bundle/utils.ts

Pullfrog  | View workflow run | via Pullfrog𝕏

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 39.82%. Comparing base (9abd347) to head (e728843).

Files with missing lines Patch % Lines
packages/custom-client/src/client.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3814      +/-   ##
==========================================
- Coverage   39.84%   39.82%   -0.02%     
==========================================
  Files         530      530              
  Lines       19475    19494      +19     
  Branches     5802     5818      +16     
==========================================
+ Hits         7759     7764       +5     
- Misses       9485     9493       +8     
- Partials     2231     2237       +6     
Flag Coverage Δ
unittests 39.82% <0.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Two correctness issues: double error-interceptor invocation in the ky client, and a type/runtime mismatch for response in the fetch client's error branch. The overall approach is sound — wrapping the full lifecycle in try/catch is the right fix for #3150 and #2204.

Good structural change overall. The pattern of extracting throwOnError/responseStyle before the try and declaring request/response as | undefined is clean and consistent across clients.

Not in diff — worth noting:

  • client-axios still has beforeRequest() outside its try block (line 72 of client-axios/bundle/client.ts), so it remains vulnerable to the exact issue this PR fixes for other clients. If this is intentional (e.g., deferred to a follow-up), it should be documented — the PR title says "all clients."
  • client-nuxt has no try/catch at all, which may be intentional given Nuxt composables' error-handling model, but should be confirmed.
Task list (5/5 completed)
  • Read the PR diff to identify major areas of change
  • Investigate high-risk areas: try-catch wrapping logic in client plugins
  • Check snapshot/test changes for correctness
  • Impact analysis: breaking change assessment
  • Draft and submit review

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run𝕏

Comment thread packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts
Comment thread packages/openapi-ts/src/plugins/@hey-api/client-fetch/bundle/types.ts Outdated
Comment thread packages/openapi-ts/src/plugins/@hey-api/client-fetch/bundle/client.ts Outdated
Comment thread packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/types.ts Outdated
SukkaW added 2 commits April 23, 2026 04:06
response/request object may become undefined after the changes, but in
our test case we can always assert they do exists.
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 22, 2026

The author's commit 932f905 made response optional in the fetch client types and the angular client runtime, but missed the angular RequestResult type definition where response was still required in the error branch. Pushed e074588 to make it optional there too, matching the fetch client and runtime behavior. Build, typecheck, and tests all pass.

Task list (5/5 completed)
  • Verify the fix in commit 932f905 for the response field typing in angular client
  • Fix remaining response required type in angular types.ts
  • Build and test to verify the fix
  • Commit, push, reply to review comment, and resolve thread
  • Report progress

Pullfrog  | View workflow run | via Pullfrog𝕏

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 22, 2026

Verified SukkaW's fixes in 932f905. The client-fetch types fix is correct — both request and response are now optional; resolved that thread. The client-angular types fix is incomplete — request was made optional but response is still required despite being undefined for non-HTTP errors; left a follow-up comment.

Task list (5/5 completed)
  • Checkout PR branch and verify SukkaW's fixes in commit 932f905
  • Verify client-angular/bundle/types.ts response is now optional
  • Verify client-fetch/bundle/types.ts response is now optional
  • Reply to addressed threads and resolve them
  • Report progress

Pullfrog  | View workflow run | via Pullfrog𝕏

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 22, 2026

Verified that e72ab28 correctly fixes the error interceptor chain issue across all four clients by passing finalError instead of the original error. Acknowledged the fix, replied to the thread, and resolved it. No code changes needed.

Task list (3/3 completed)
  • Review the fix commit e72ab28 to verify the error interceptor chain issue is resolved
  • Reply to the review comment acknowledging the fix
  • Resolve the thread and report progress

Pullfrog  | View workflow run | via Pullfrog𝕏

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 22, 2026

Replied to SukkaW's feedback on the ky client double error-interceptor invocation thread. After re-reading the code, the original finding still holds: when throwOnError: true, parseErrorResponse throws (line 110) instead of returning, so the throw escapes the inner catch and is re-caught by the outer catch — running error interceptors twice. Explained the specific control flow in the reply.

Task list (3/3 completed)
  • Review the ky client code to verify SukkaW's claim about parseErrorResponse not causing double invocation
  • Reply to the review comment with findings
  • Report progress

Pullfrog  | View workflow run | via Pullfrog𝕏

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 22, 2026

Verified and confirmed the fix in 895ba87. The errorInterceptorsInvoked flag correctly prevents double error-interceptor invocation in client-ky. Replied to the thread with confirmation and a minor comment nit, then resolved it.

Task list (4/4 completed)
  • Checkout PR branch and verify the fix in commit 895ba87
  • Review the fix for correctness
  • Reply to review comment and resolve thread
  • Report progress

Pullfrog  | View workflow run | via Pullfrog𝕏

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 22, 2026

Open in StackBlitz

@hey-api/codegen-core

npm i https://pkg.pr.new/@hey-api/codegen-core@3814

@hey-api/json-schema-ref-parser

npm i https://pkg.pr.new/@hey-api/json-schema-ref-parser@3814

@hey-api/nuxt

npm i https://pkg.pr.new/@hey-api/nuxt@3814

@hey-api/openapi-ts

npm i https://pkg.pr.new/@hey-api/openapi-ts@3814

@hey-api/shared

npm i https://pkg.pr.new/@hey-api/shared@3814

@hey-api/spec-types

npm i https://pkg.pr.new/@hey-api/spec-types@3814

@hey-api/types

npm i https://pkg.pr.new/@hey-api/types@3814

@hey-api/vite-plugin

npm i https://pkg.pr.new/@hey-api/vite-plugin@3814

commit: e728843

@SukkaW
Copy link
Copy Markdown
Contributor Author

SukkaW commented Apr 22, 2026

@mrlubos I believe the PR is ready for review now, and

As usual, I didn't include a changeset in this PR. Feel free to push one as you see fit for the actual CHANGELOG.

@SukkaW SukkaW changed the title fix(#2204/#3150)!: wrap all clients request in the try catch fix(#2204/#3150/#3519)!: proper error handling Apr 22, 2026
@SukkaW SukkaW changed the title fix(#2204/#3150/#3519)!: proper error handling fix(#2204/#3150/#3519)!: proper error handling for all clients Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug 🔥 Broken or incorrect behavior. size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorectly typed error interceptor of fetch-client Errors from Zod validator does not trigger the error interceptor ThrowOnError not working

1 participant