Skip to content

fix(client-next): build URL after request interceptors#3804

Open
jnsdls wants to merge 2 commits intohey-api:mainfrom
jnsdls:fix/client-next-interceptor-order
Open

fix(client-next): build URL after request interceptors#3804
jnsdls wants to merge 2 commits intohey-api:mainfrom
jnsdls:fix/client-next-interceptor-order

Conversation

@jnsdls
Copy link
Copy Markdown

@jnsdls jnsdls commented Apr 21, 2026

Summary

Fixes #3803.

@hey-api/client-next built the request URL before request interceptors ran. That meant request interceptors could mutate opts.baseUrl, opts.url, opts.path, or opts.query, but the fetch call still used the URL that had already been computed from the original options.

This PR moves buildUrl(opts) to after the request interceptor loop so the final fetch URL reflects interceptor mutations. The SSE path keeps the same behavior by seeding createSseClient with the initial URL while still allowing its internal onRequest hook to apply per-request interceptor mutations.

This is specific to client-next: other clients that pass Request objects through interceptors do not need this recomputation step.

Changes

  • Defer buildUrl(opts) in packages/openapi-ts/src/plugins/@hey-api/client-next/bundle/client.ts until after request interceptors run.
  • Add regression coverage for request interceptor mutations to baseUrl, url, path, and query.
  • Refresh generated @hey-api/client-next snapshots, including the SSE Next.js fixture.
  • Add a patch changeset for @hey-api/openapi-ts.

Test plan

  • pnpm vitest run packages/openapi-ts/src/plugins/@hey-api/client-next/__tests__/client.test.ts
  • pnpm vitest run --project @test/openapi-ts packages/openapi-ts-tests/main/test/clients.test.ts -t '@hey-api/client-next'
  • pnpm vitest run --project @test/openapi-ts packages/openapi-ts-tests/main/test/3.1.x.test.ts -t 'client with SSE (Next.js)'

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

🦋 Changeset detected

Latest commit: 619f675

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 Patch

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

@jnsdls 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 size:XS This PR changes 0-9 lines, ignoring generated files. bug 🔥 Broken or incorrect behavior. labels Apr 21, 2026
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 21, 2026

TL;DR — Fixes two bugs in the generated HTTP client templates: client-next now builds the request URL after request interceptors run (so interceptor mutations to baseUrl/url/path/query are actually honored), and all three clients (client-next, client-fetch, client-ky) now thread finalError through the error interceptor chain instead of always passing the original error. Adds regression tests for both fixes.

Key changes

  • Move buildUrl() after request interceptors in client-nextbeforeRequest() no longer pre-computes the URL; it is built after interceptors have had a chance to mutate opts
  • Thread finalError through error interceptor chains — error interceptors in client-fetch, client-ky, and client-next now receive the previous interceptor's output instead of the original error, aligning with client-angular and client-ofetch
  • Add regression tests for both fixesclient-next gets four tests verifying interceptor mutations to baseUrl, url, path, and query are honored; all three clients get chain-composition tests proving finalError flows through
  • Document behavioral change in changeset — the changeset now includes a detailed migration note warning that users with ≥2 error interceptors will see different error payloads after upgrading
  • Update generated client.gen.ts snapshots — mechanical propagation of the two source-level fixes across all test fixtures and examples

Summary | 200 files | 4 commits | base: mainfix/client-next-interceptor-order


Deferred URL construction in client-next

Before: buildUrl(opts) was called inside beforeRequest(), before request interceptors ran — any interceptor mutation to opts.baseUrl, opts.url, opts.path, or opts.query was silently ignored.
After: buildUrl(opts) runs after the request interceptor loop, so all mutations are reflected in the final fetch URL.

The request() function now destructures only { opts } from beforeRequest() and calls buildUrl(opts) once interceptors have finished. The same pattern is applied to the SSE path in makeSseFn, with a comment explaining that SSE re-applies interceptors internally.

Why doesn't this affect client-fetch and client-ky? Those clients pass a Request object through interceptors, so URL mutations happen on the request object itself and are picked up automatically. client-next passes plain opts, making explicit URL recomputation necessary.

client-next/bundle/client.ts · client-next/__tests__/client.test.ts


Error interceptor chain composition fix

Before: Every error interceptor received the original error, discarding transformations from earlier interceptors in the chain.
After: Each interceptor receives finalError — the accumulated result of all preceding interceptors.

A single-token change (errorfinalError) in the error interceptor loop of each client template. This matches the existing correct behavior in client-angular and client-ofetch. New regression tests in all three affected clients verify that the second interceptor sees the first interceptor's output and the final result.error carries transformations from the entire chain.

Is this a breaking change? Technically yes for users with ≥2 error interceptors. The changeset includes a migration note: if you relied on the previous "only the last interceptor wins" behavior, error payloads will differ after upgrading. Interceptors should always return an error-shaped value — returning a falsy value (e.g. undefined) clears the accumulated error for subsequent interceptors.

client-fetch/bundle/client.ts · client-ky/bundle/client.ts · client-next/bundle/client.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.

Reviewed — no issues found.

Both fixes are correct and well-scoped. Moving buildUrl() after request interceptors in client-next ensures URL mutations from interceptors are honored, and threading finalError through the error chain aligns client-fetch, client-ky, and client-next with the already-correct behavior in client-angular and client-ofetch. All 183 snapshot changes are mechanical propagation of the three source template edits.

Task list (4/4 completed)
  • Read changeset and PR description for context
  • Review core client source changes (client-fetch, client-ky, client-next)
  • Review snapshot changes for consistency
  • Self-critique and submit review

Pullfrog  | View workflow run𝕏

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

❌ Patch coverage is 60.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.58%. Comparing base (982e431) to head (619f675).

Files with missing lines Patch % Lines
.../src/plugins/@hey-api/client-next/bundle/client.ts 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3804      +/-   ##
==========================================
- Coverage   39.58%   39.58%   -0.01%     
==========================================
  Files         532      532              
  Lines       19580    19581       +1     
  Branches     5829     5835       +6     
==========================================
  Hits         7751     7751              
- Misses       9581     9582       +1     
  Partials     2248     2248              
Flag Coverage Δ
unittests 39.58% <60.00%> (-0.01%) ⬇️

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.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:XS This PR changes 0-9 lines, ignoring generated files. labels Apr 21, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 21, 2026

Open in StackBlitz

@hey-api/codegen-core

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

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

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

@hey-api/nuxt

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

@hey-api/openapi-ts

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

@hey-api/shared

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

@hey-api/spec-types

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

@hey-api/types

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

@hey-api/vite-plugin

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

commit: 619f675

@jnsdls
Copy link
Copy Markdown
Author

jnsdls commented Apr 21, 2026

👋 Heads-up for reviewers: the error-interceptor fix in this PR is a subtle behavioral change for anyone who installed 2 or more error interceptors.

Before this PR: each interceptor received the original error, and only the last interceptor's return value survived. A chain of N interceptors effectively behaved as if only the final one was installed.

After this PR: each interceptor receives the previous interceptor's output (standard middleware composition — matches request/response interceptors, and matches how client-angular / client-ofetch already worked).

Gotcha to watch for: if an interceptor returns a falsy value (e.g. undefined) mid-chain, the accumulated error is cleared for the remainder of the chain. Interceptors should always return an error-shaped value.

This caveat is now explicitly called out in both the changeset and the PR description. Flagging here so it's visible in the PR timeline before merge.

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Apr 28, 2026

@jnsdls the error interceptor fix landed as part of #3814, would you be able to apply your changes on main?

@jnsdls jnsdls force-pushed the fix/client-next-interceptor-order branch from 7f788f0 to 29a6629 Compare April 29, 2026 08:32
@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Apr 29, 2026
@jnsdls
Copy link
Copy Markdown
Author

jnsdls commented Apr 29, 2026

Thanks for the heads-up. I refreshed the branch onto current main and removed the error-interceptor portion that landed in #3814.

This PR now only contains the @hey-api/client-next URL construction fix, its focused regression tests, the generated client-next snapshots, and the simplified changeset.

Verified locally with:

  • pnpm vitest run packages/openapi-ts/src/plugins/@hey-api/client-next/__tests__/client.test.ts
  • pnpm vitest run --project @test/openapi-ts packages/openapi-ts-tests/main/test/clients.test.ts -t '@hey-api/client-next'
  • pnpm vitest run --project @test/openapi-ts packages/openapi-ts-tests/main/test/3.1.x.test.ts -t 'client with SSE (Next.js)'

@jnsdls jnsdls changed the title fix(clients): build URL after request interceptors + thread finalError through error chain fix(client-next): build URL after request interceptors Apr 29, 2026
@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 Apr 29, 2026
@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Apr 29, 2026

@pullfrog review the latest version. Is this issue exclusive to the Next.js client?

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 29, 2026

Reviewed the latest version and submitted an approval. The bug is effectively exclusive to client-next due to its unique interceptor contract (interceptors mutate opts in place, unlike other clients which return a modified Request object).

Task list (4/4 completed)
  • Checkout PR and read the diff
  • Investigate whether the fix is exclusive to Next.js client
  • Review code changes for correctness
  • Submit review

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.

The fix is correct and well-tested. To answer the question of whether this bug is exclusive to client-nextyes, effectively it is, because of an architectural difference in how interceptors work across clients.

In client-next, request interceptors receive the raw options object and mutate it in place (await fn(opts)). This means mutations to opts.baseUrl, opts.url, opts.path, or opts.query are the intended way to modify the request URL. The bug was that buildUrl was called before interceptors ran, so these mutations had no effect.

The other clients (client-fetch, client-ky, client-ofetch, client-angular) pass a Request object to interceptors (request = await fn(request, opts)). Users change the URL by returning a new Request with a different URL — they don't rely on buildUrl being re-invoked. So while buildUrl is also called before interceptors in those clients, it's not a bug because the interceptor contract is different (return a modified Request, not mutate opts).

client-axios delegates to Axios's own interceptor system (which operates on the config object including url), and client-nuxt uses ofetch's onRequest pattern. Neither suffers from the same issue.

One minor note on the SSE path: buildUrl(opts) is called before the SSE interceptors in onRequest, but the comment correctly explains that onRequest runs interceptors per-request and rebuilds via new Request(requestInit.url, requestInit). However, only direct requestInit.url mutations work there — baseUrl/path/query mutations on the cast requestInit object wouldn't trigger a URL rebuild. This is a pre-existing limitation (not introduced by this PR) and probably acceptable for the SSE use case.

Task list (4/4 completed)
  • Checkout PR and read the diff
  • Investigate whether the fix is exclusive to Next.js client
  • Review code changes for correctness
  • Submit review

Pullfrog  | View workflow run𝕏

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.

@hey-api/client-next: buildUrl called before request interceptors, so interceptor mutations to baseUrl/url/path/query are ignored by fetch()

2 participants