Skip to content

feat(openapi-ts): add typed FetchError for fetch client#3844

Open
Krisztiaan wants to merge 1 commit intohey-api:mainfrom
Krisztiaan:fetch-error-wrapper
Open

feat(openapi-ts): add typed FetchError for fetch client#3844
Krisztiaan wants to merge 1 commit intohey-api:mainfrom
Krisztiaan:fetch-error-wrapper

Conversation

@Krisztiaan
Copy link
Copy Markdown

@Krisztiaan Krisztiaan commented Apr 30, 2026

Summary

  • add an exported FetchError<TError> wrapper for @hey-api/client-fetch
  • add opt-in fetch client option throwOnErrorStyle: 'wrapper' for throwOnError failures
  • preserve typed backend error bodies on FetchError.error while exposing request, response, status, statusText, and headers
  • keep TanStack Query generated errors as raw operation error bodies by default, and generate FetchError<OperationError> only when the fetch client plugin opts into throwOnErrorStyle: 'wrapper'
  • document default and opt-in fetch/TanStack error handling, including network/abort cases without HTTP response metadata

Compatibility

  • Default Fetch behavior is preserved.
  • throwOnError: false keeps the existing returned result shape: typed error body plus request/response fields.
  • throwOnError: true still throws the raw parsed backend error body by default.
  • Fetch + TanStack Query still exposes raw operation-specific error body types by default.
  • Users opt into the wrapper by configuring @hey-api/client-fetch with throwOnErrorStyle: 'wrapper'; then thrown errors and generated TanStack error generics use FetchError<OperationError>.
  • Error interceptors still receive the parsed backend body or original non-HTTP error before optional wrapping. If an interceptor returns a FetchError, the client rethrows it without double wrapping.
  • Network errors and aborted requests are wrapped only in wrapper mode, and response, status, statusText, and headers can be undefined when no HTTP response exists.

Release

  • Added .changeset/fetch-error-wrapper.md as a minor changeset for @hey-api/openapi-ts.

Verification

  • pnpm --filter @hey-api/openapi-ts build
  • pnpm exec vitest run --config vitest.local.config.mts src/plugins/@hey-api/client-fetch/__tests__/client.test.ts (package-local temporary config, removed)
  • pnpm exec vitest run --config vitest.local-no-examples.config.mts --project @test/openapi-ts --project @test/openapi-ts-nestjs-v11 --project @test/openapi-ts-orpc-v1 --project @test/openapi-ts-sdks --project @test/openapi-ts-valibot-v1 --project @test/openapi-ts-tanstack-query-v5 --project @test/openapi-ts-zod-v3 --project @test/openapi-ts-zod-v4 --update (temporary config, removed)
  • pnpm examples:check
  • pnpm --filter @hey-api/openapi-ts typecheck
  • pnpm --filter @test/openapi-ts-tanstack-query-v5 typecheck

Note: local root vitest --project ... and full pnpm build can fail in this environment before reaching the requested project because Vue devtools calls localStorage.getItem while loading example configs. GitHub CI is authoritative for those full-matrix checks and is passing except for Vercel authorization.

Copilot AI review requested due to automatic review settings April 30, 2026 12:05
@bolt-new-by-stackblitz
Copy link
Copy Markdown

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

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

🦋 Changeset detected

Latest commit: 2a3e2d1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hey-api/openapi-ts Minor

Not sure what this means? Click here to learn what changesets are.

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

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

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

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Apr 30, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@dosubot dosubot Bot added the feature 🚀 Feature request. label Apr 30, 2026
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 30, 2026

TL;DR — Introduces an opt-in throwOnErrorStyle: 'wrapper' config for @hey-api/client-fetch that wraps thrown errors in a typed FetchError<TError> class, exposing the parsed error body alongside HTTP metadata. TanStack Query codegen emits FetchError<OperationError> as the error type when this mode is active.

Key changes

  • Add FetchError<TError> class to the fetch client — A generic Error subclass exposing .error (typed body), .status, .statusText, .headers, .request, and .response from the failed fetch.
  • Introduce throwOnErrorStyle config option — When set to 'wrapper', the throwOnError path wraps errors in FetchError instead of throwing the raw body. Default ('body') preserves existing behavior.
  • Type TanStack Query error generics as FetchError<OperationError> — The codegen emits FetchError-wrapped error types and passes throwOnErrorStyle: 'wrapper' in SDK call options when the client is configured for it.
  • Extract sdkCallOptions helper for TanStack Query codegen — Centralizes the SDK call options construction (including conditional throwOnErrorStyle) used by queryOptions, mutationOptions, and infiniteQueryOptions.
  • Add tests and documentation — New test cases covering HTTP errors, interceptor integration, abort scenarios, and network errors; docs added for the fetch client and TanStack Query plugin.

Summary | 594 files | 1 commit | base: mainfetch-error-wrapper


FetchError class and opt-in wrapping

Before: throwOnError: true threw the raw parsed error body directly — no standard shape, no HTTP metadata attached.
After: Setting throwOnErrorStyle: 'wrapper' wraps all thrown errors in FetchError<TError>, which extends Error and exposes .error, .status, .statusText, .headers, .request, and .response.

The class lives in bundle/utils.ts and uses a getFetchErrorMessage helper to derive Error.message from either the response status line, the original error's message, or a fallback "Request failed". Network errors and aborted requests are also wrapped but have undefined response metadata. Double-wrapping is prevented by checking instanceof FetchError before constructing a new one.

Why opt-in instead of default? The default throwOnErrorStyle: 'body' preserves backward compatibility — existing code that catches the raw error body continues to work unchanged. Consumers explicitly opt in to the richer FetchError shape.

bundle/utils.ts · bundle/client.ts · bundle/types.ts · types.ts


TanStack Query typed error generics and sdkCallOptions helper

Before: Generated queryOptions / mutationOptions used the bare operation error type and inlined throwOnError: true at each call site.
After: A new sdkCallOptions helper centralizes the options construction, conditionally adding throwOnErrorStyle: 'wrapper'. The error type parameter becomes FetchError<OperationError> when the wrapper style is active.

The helper inspects the resolved client plugin config at codegen time and appends the property only when relevant, keeping the generated output identical for non-wrapper configurations. The useType.ts module registers a FetchError symbol import pointing to the generated client folder, mirroring how @hey-api/client-axios uses AxiosError<TError> for its error generics.

sdkCall.ts · useType.ts · queryOptions.ts · mutationOptions.ts


Tests and documentation

Before: No tests for FetchError behavior; no error handling docs for the fetch client.
After: New test cases validate default throw behavior, wrapper throw behavior (HTTP errors, empty status text, interceptor results, abort errors), and non-interceptor paths. Docs added to both the fetch client page and the TanStack Query plugin page explaining the opt-in pattern.

The tests cover: raw body throw (default), FetchError with HTTP metadata (wrapper), empty statusText message formatting, FetchError with error interceptor result, AbortError wrapped as FetchError, and a snapshot test for TanStack Query codegen output with throwOnErrorStyle: 'wrapper'.

client.test.ts · fetch.md · tanstack-query.md

Pullfrog  | View workflow run | via Pullfrog𝕏

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

Missing changeset and one minor message formatting nit. Overall the implementation is solid — the class design, interceptor passthrough, double-wrap guard, and TanStack Query type integration all look correct.

Task list (5/5 completed)
  • Checkout PR and read the diff
  • Identify major areas of change and high-risk sections
  • Deep review of implementation changes
  • Impact analysis - check for breaking changes
  • Self-critique and submit review

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

Comment thread packages/openapi-ts/src/plugins/@hey-api/client-fetch/bundle/utils.ts Outdated
return 'Request failed';
};

export class FetchError<TError = unknown> extends Error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a breaking change for throwOnError: true users — caught values change from raw error bodies to FetchError instances. A changeset is required for the release pipeline. Given it changes what throw produces, this likely warrants a major version bump for @hey-api/client-fetch (or at minimum a minor with clear migration notes).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Verified against the current implementation: this is now opt-in via throwOnErrorStyle: 'wrapper', so the default throwOnError: true behavior still throws the raw backend body. Added .changeset/fetch-error-wrapper.md as a minor release note for the new opt-in FetchError API.

@Krisztiaan Krisztiaan force-pushed the fetch-error-wrapper branch 3 times, most recently from 3e444c9 to 9dac82f Compare April 30, 2026 12:30
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Apr 30, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 39.65%. Comparing base (982e431) to head (2a3e2d1).
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3844      +/-   ##
==========================================
+ Coverage   39.58%   39.65%   +0.07%     
==========================================
  Files         532      533       +1     
  Lines       19580    19616      +36     
  Branches     5829     5842      +13     
==========================================
+ Hits         7751     7779      +28     
- Misses       9581     9589       +8     
  Partials     2248     2248              
Flag Coverage Δ
unittests 39.65% <ø> (+0.07%) ⬆️

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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 30, 2026

Open in StackBlitz

@hey-api/codegen-core

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

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

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

@hey-api/nuxt

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

@hey-api/openapi-ts

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

@hey-api/shared

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

@hey-api/spec-types

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

@hey-api/types

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

@hey-api/vite-plugin

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

commit: 2a3e2d1

@Krisztiaan Krisztiaan force-pushed the fetch-error-wrapper branch from 9dac82f to 4932339 Compare April 30, 2026 12:44
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 30, 2026

Acknowledged the author's fix for the HTTP/2 statusText issue and resolved the thread — no code changes needed on my end.

Pullfrog  | View workflow run | via Pullfrog𝕏

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 30, 2026

Review submitted. Leaping into action...

Pullfrog  | View workflow run | via Pullfrog𝕏

@Krisztiaan Krisztiaan changed the title Add typed FetchError for fetch client feat(openapi-ts): add typed FetchError for fetch client Apr 30, 2026
@Krisztiaan Krisztiaan force-pushed the fetch-error-wrapper branch from 4932339 to 2a3e2d1 Compare April 30, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 🚀 Feature request. size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants