Skip to content

fix(client-ofetch): set ignoreResponseError default to false and handle FetchError response#3856

Open
awdr74100 wants to merge 3 commits intohey-api:mainfrom
awdr74100:fix/client-ofetch-ignore-response-error
Open

fix(client-ofetch): set ignoreResponseError default to false and handle FetchError response#3856
awdr74100 wants to merge 3 commits intohey-api:mainfrom
awdr74100:fix/client-ofetch-ignore-response-error

Conversation

@awdr74100
Copy link
Copy Markdown

Problem

onResponseError was never called for 4xx/5xx responses because ignoreResponseError: true was
hardcoded as the default, preventing ofetch from triggering its error hook. This contradicts the
documented usage.

Even setting ignoreResponseError: false manually did not help — client.ts had no handling for the
FetchError thrown by ofetch, so result.response would be undefined and result.error would be the raw
FetchError object instead of the parsed response body.

Fix

  • Changed ignoreResponseError default from true to false in createConfig() and buildOfetchOptions()
  • Added an inner try/catch around $ofetch.raw() in client.ts to extract the response from FetchError
    before continuing through the response interceptor pipeline

This ensures:

  • onResponseError fires as expected for non-2xx responses
  • client.interceptors.response still runs for all responses including 4xx
  • result.response and result.error are correctly populated on error

Test plan

  • response interceptors still run for 4xx responses when ofetch throws
  • error payload is correctly returned for 4xx
  • network errors (no response) are still handled via error interceptors

@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 May 7, 2026

@awdr74100 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 May 7, 2026

⚠️ No Changeset found

Latest commit: e6770ed

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

@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. bug 🔥 Broken or incorrect behavior. labels May 7, 2026
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented May 7, 2026

TL;DR — Fixes client-ofetch so onResponseError and client.interceptors.response fire for non-2xx responses, and so result.response / result.error are populated correctly when ofetch throws a FetchError.

Key changes

  • Flip ignoreResponseError default to false — aligns runtime behavior with the documented usage so ofetch's error hooks trigger for 4xx/5xx.
  • Catch FetchError around $ofetch.raw() — extract the underlying Response so the response interceptor pipeline still runs and network errors still propagate.
  • Cover the new paths with tests — dedicated suites for the default flip, the opt-out path, the FetchError catch, and network-error handling in both client.test.ts and utils.test.ts.
  • Regenerate all client-ofetch snapshots and the ofetch example — the 11 client-ofetch variant snapshots plus sse-ofetch and examples/openapi-ts-ofetch now match the bundle changes so pnpm examples:check and snapshot tests stay green.

Summary | 30 files | 3 commits | base: mainfix/client-ofetch-ignore-response-error


ignoreResponseError default flipped to false

Before: createConfig() and buildOfetchOptions() hardcoded ignoreResponseError: true, so ofetch never threw and onResponseError never fired for non-2xx responses.
After: Both default to false, matching the client's documented behavior and letting ofetch's error hooks run.

Users can still opt back in by passing ignoreResponseError: true explicitly — the ?? fallback preserves overrides.

bundle/utils.ts · utils.gen.ts


FetchError unwrapping inside request

Before: With ignoreResponseError: false, ofetch threw FetchError out of $ofetch.raw(). Nothing in client.ts handled it, so response stayed unset, response interceptors were skipped, and result.error was the raw FetchError instead of the parsed body.
After: An inner try/catch extracts fetchError.response and continues through the response interceptor pipeline, so result.response / result.error are populated as expected. Errors without a response (network failures) are rethrown to the outer error path.

Why catch here instead of relying on ofetch's `onResponseError`? The response interceptor chain lives downstream of `$ofetch.raw()` in this client. If the promise rejects, that chain never runs. Unwrapping `FetchError.response` lets the same interceptor pass handle 2xx and non-2xx uniformly, while rejections without a `response` still bubble to the outer catch for error interceptors.

bundle/client.ts · client.gen.ts


Tests for default, opt-out, non-2xx, and network-error paths

Before: No coverage for the throwing branch of $ofetch.raw() and no tests pinning the ignoreResponseError default.
After: New ignoreResponseError config and non-2xx response handling suites in client.test.ts assert the default value, that ignoreResponseError is forwarded to ofetch.raw, that the opt-out path still surfaces { error, response } without entering the new catch, that response interceptors run on 4xx, that result.error / result.response are populated, and that bare network errors bubble through the error path with response undefined. utils.test.ts adds matching unit tests for buildOfetchOptions defaults and opt-in.

__tests__/client.test.ts · __tests__/utils.test.ts


Snapshot and example regeneration

Before: The previous push updated only the bundle/ sources and the openapi-ts-ofetch example, leaving the 11 client-ofetch variant snapshots and sse-ofetch out of sync with the bundle.
After: The new fix(client-ofetch): update snapshot commit regenerates every client-ofetch variant (base-url-{false,number,strict,string}, clean-false, default, import-file-extension-ts, sdk-client-{optional,required}, tsconfig-{node16,nodenext}-sdk) plus sse-ofetch so snapshot tests match the new try/catch and ignoreResponseError: false default.

default/client.gen.ts · default/utils.gen.ts · sse-ofetch/client.gen.ts

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

The runtime fix in bundle/client.ts and bundle/utils.ts is correct — I verified every load-bearing claim about ofetch against upstream source (unjs/ofetch): .raw() does throw FetchError on non-2xx when ignoreResponseError: false, FetchError.response is the same FetchResponse<T> with ._data populated before the error is constructed, and network failures yield a FetchError with .response === undefined which the new catch correctly rethrows.

However this PR is a breaking behavioral change for existing client-ofetch users (4xx/5xx responses now throw inside ofetch and flow through a different path), and it's missing the artifacts that ship with that kind of change in this repo:

  1. All generated snapshots are stale — all 11 dirs under packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-ofetch/**/client/utils.gen.ts still contain ?? true on line 304 and ignoreResponseError: true on line 504, plus none of them contain the new try { $ofetch.raw(...) } catch { ... } block. examples/openapi-ts-ofetch/src/client/client/utils.gen.ts is also stale (same two lines). Snapshot tests will fail on CI until these are regenerated (pnpm tu -- @hey-api/openapi-ts + pnpm examples:generate).
  2. No changeset.changeset/ contains only README.md. Per docs/openapi-ts/community/contributing/developing.md and the precedent of #3814, semantic client-behavior flips require a changeset marked **BREAKING**:.
  3. No docs/openapi-ts/migrating.md entry — the v0.97.0 block documents analogous client breaking changes; this PR needs a peer entry under the next version.
  4. docs/openapi-ts/clients/ofetch.md never documents ignoreResponseError or the new error-flow; at minimum, users need to know how to restore the pre-PR behavior (ignoreResponseError: true).

Inline comments cover a test-coverage gap for the central change. Nothing blocks from a correctness standpoint — the control flow is sound and the FetchError extraction path is faithful to ofetch's contract.

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

...jsonBodySerializer,
headers: defaultHeaders,
ignoreResponseError: true,
ignoreResponseError: false,
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.

The central behavioral change of this PR is the flip from true to false here and at line 302, but no added test observes either value. A regression reverting one (or a typo letting the two defaults diverge) would pass the new suite because every new test uses mockRejectedValue(...) and never inspects the ofetch options. Consider adding expect(createClient({}).getConfig().ignoreResponseError).toBe(false) and/or a test that captures mockOfetch.raw.mock.calls[0][1].ignoreResponseError — the existing serialized request body handling suite already uses that pattern.


expect(result.error).toEqual({ message: 'Not Found' });
expect(result.response?.status).toBe(404);
});
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.

There's no test asserting the opt-out path (ignoreResponseError: true) still works as a backward-compat escape hatch. A future change forcing the option to false regardless of user input would go unnoticed. Consider a fourth test that passes ignoreResponseError: true, uses mockResolvedValue(response404) (not rejected), and asserts the client returns { error, response } without entering the new catch branch.

Comment on lines +183 to +195
try {
response = await $ofetch.raw(finalUrl, responseOptions);
} catch (fetchError: unknown) {
// When ignoreResponseError is false (default), ofetch throws FetchError for non-2xx.
// Extract the response so response interceptors can still process it before we
// decide whether to throw or return the error.
const maybeFetchError = fetchError as { response?: typeof response };
if (maybeFetchError?.response) {
response = maybeFetchError.response;
} else {
throw fetchError;
}
}
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.

Not blocking, but worth noting: real ofetch FetchError also exposes .status, .statusText, and .data as getters on the error itself (see unjs/ofetch src/error.ts). The mock in the new tests only mirrors .response, which is fine for what this block reads today but would invisibly drift from reality if fetchError.status-based branching (e.g. retry on specific codes) is added later. Not a change request — a note for whoever extends this catch.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels May 7, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 39.68%. Comparing base (1721aa6) to head (e6770ed).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3856      +/-   ##
==========================================
+ Coverage   39.58%   39.68%   +0.10%     
==========================================
  Files         532      532              
  Lines       19581    19587       +6     
  Branches     5835     5831       -4     
==========================================
+ Hits         7751     7773      +22     
+ Misses       9582     9572      -10     
+ Partials     2248     2242       -6     
Flag Coverage Δ
unittests 39.68% <100.00%> (+0.10%) ⬆️

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 May 7, 2026

Open in StackBlitz

@hey-api/codegen-core

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

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

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

@hey-api/nuxt

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

@hey-api/openapi-ts

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

@hey-api/shared

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

@hey-api/spec-types

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

@hey-api/types

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

@hey-api/vite-plugin

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

commit: e6770ed

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:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant