Skip to content

feat(core)!: v2 — safer defaults, file transfer, and pagination (#37–#46) - #47

Open
developerEhsan wants to merge 3 commits into
masterfrom
fix/v2-consumer-issues
Open

developerEhsan wants to merge 3 commits into
masterfrom
fix/v2-consumer-issues

Conversation

@developerEhsan

Copy link
Copy Markdown
Owner

Closes #37, #38, #39, #40, #41, #42, #43, #44, #45, #46.

All ten issues came from one production consumer. Three change existing defaults — in each case because the current default is the unsafe one — so this is a major release.

Breaking

# Change Migration
#40 Response cache defaults to off cache: { enabled: true } to keep it
#42 Only idempotent methods retry retry: { retryUnsafeMethods: true } to keep it
#41 Next.js helpers moved to /next import … from '@developerehsan/api-client/next'

Full notes: docs/migrating-v2.md.

#40 — the cache was on with a 60s cache-first TTL and no mention in the README. A write would commit, the app's own query layer would refetch, and this client's cache would still serve the stale value underneath it. Apps pairing it with TanStack Query were silently double-cached.

#42 — a POST that fails with a 5xx or a dropped connection may already have committed server-side; retrying it creates the resource twice. Now only GET/HEAD/OPTIONS/TRACE/PUT/DELETE retry. http.idempotency generates one key per logical call and replays it across that call's retries, so an opted-in write retry is collapsible server-side.

#41 — the root entry carried a dynamic import('next/headers'), and Vite's optimizeDeps scan doesn't honor @vite-ignore inside a pre-bundled dependency. The specifier is now assembled at runtime and the helpers live behind /next, so no non-Next consumer pulls it in. Verified: dist/index.js, index.cjs and edge.js contain zero references to it.

Added

Security notes

The RPC trust boundary is unchanged in shape — everything new is deny-by-default and server-declared:

  • binary is a separate allowlist; a method must be in both expose and binary. A client-supplied responseType is still stripped by sanitizePerCall (S4), so the browser cannot turn a JSON method into a byte stream. There's a test for exactly that.
  • multipart is opt-in and documented as leaning entirely on the Origin check, since a multipart body is a CORS "simple request" and so isn't covered by the JSON-content-type half of the CSRF defence. Keep csrf: true with it.
  • Binary filenames are sanitized against Content-Disposition header injection.
  • A binary method inside a batch is refused rather than JSON-mangled — a batch is one JSON array, with nowhere for a byte stream to go.

Two issues found while reviewing this diff and fixed in it:

  • Unbounded body read. The size cap was enforced against the client-supplied Content-Length, which can lie or be absent under chunked encoding. Now bounded against the bytes actually read, aborting the stream past the cap. This bug pre-existed on the JSON path; raising the limit to 10 MB for uploads would have widened it considerably.
  • Prototype-pollution bypass. Multipart envelope revival built objects with out[k] = v; with the key __proto__ that invokes the prototype setter, so the key disappears from Object.keys and slips past the handler's existing FORBIDDEN_KEYS guard. Now uses defineProperty, keeping it an own property so that guard still fires. Covered by a test verified to fail without the fix.

Verification

  • 387 core tests pass (29 new, in packages/core/src/v2-consumer-issues.test.ts), plus tanstack-query, vite-plugin and the examples.
  • tsc --noEmit and Biome clean across the workspace.
  • Browser-bundle guard holds: dist/browser.js contains no axios, openapi, or backend detail.

🤖 Generated with Claude Code

Ten issues reported by a production consumer. Three change existing
defaults, each because the old default was the unsafe one.

BREAKING CHANGES

* Response cache now defaults to off (#40). It was on with a 60s
  cache-first TTL, serving stale reads underneath an app's own query
  layer after a write. Pass `cache: { enabled: true }` to keep it.
* Unsafe HTTP methods are no longer retried (#42). A POST that fails
  with a 5xx or a dropped connection may already have committed, so a
  blind retry double-creates. Opt back in with `retryUnsafeMethods`.
* Next.js server-context helpers moved to the `/next` subpath (#41).
  The root entry no longer references `next/headers`, which was
  breaking Vite `optimizeDeps` in projects without Next installed.

Added

* `http.idempotency` — one key per logical call, replayed across its
  retries, so an opted-in write retry is collapsible server-side (#42).
* `errorEnvelope` — teach the classifier a nested error shape, so
  `error.message` is the server's message and not a JSON blob (#39).
* `paginate` / `paginateItems` / `collectPages` for cursor pagination,
  with same-cursor and maxPages guards (#43).
* `createIndexedDbStore({ crossTab: true })` — cross-tab invalidation
  over BroadcastChannel, matching the Redis store (#46).
* FormData bodies on the direct client and over the RPC bridge, via
  `createRpcRouteHandler({ multipart: true })` (#38).
* Binary responses through the RPC bridge via a server-declared
  `binary` allowlist (#37). Client-supplied responseType still cannot
  force it — sanitizePerCall (S4) is unchanged.
* `openapi` is optional on GlobalConfig (#45).

Fixed

* A configured `Content-Type: application/json` no longer corrupts a
  FormData body by suppressing the multipart boundary (#38).
* Request bodies are now size-capped against the bytes actually read
  rather than the client-supplied Content-Length, which can lie or be
  absent under chunked encoding. Pre-existing on the JSON path.
* Multipart envelope revival uses defineProperty, so a `__proto__` key
  stays an own property and is still caught by the handler's
  FORBIDDEN_KEYS guard instead of silently moving to the prototype.
* Removed a stale "stubbed with TODOs" note from createClient's
  shipped JSDoc that contradicted the README (#44).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
api-client-nextjs Skipped Skipped Oct 21, 2026 12:14pm UTC
api-client-react-vite Skipped Skipped Oct 21, 2026 12:14pm UTC

@developerEhsan developerEhsan changed the title v2: safer defaults, file transfer, and pagination (closes #37–#46) feat(core)!: v2 — safer defaults, file transfer, and pagination (#37–#46) Oct 21, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel
vercel Bot temporarily deployed to Preview – api-client-react-vite October 21, 2026 12:10 Inactive
@vercel
vercel Bot temporarily deployed to Preview – api-client-nextjs October 21, 2026 12:10 Inactive
Cancelling the request-body stream closes it while the runtime may still
be enqueueing already-received chunks, which surfaces as
ERR_INVALID_STATE ('ReadableStream is already closed') thrown inside
undici. That is an unhandled rejection no caller can catch, and an
unhandled rejection can take down a server process.

Caught by CI on Node 24 (all 387 tests passed, but the run reported one
unhandled error); it is a timing-dependent race that did not reproduce
locally across repeated runs on the same version.

Stopping the read and releasing the lock is sufficient: the caller
responds 413 immediately and the runtime tears the request down. The
size cap itself is unchanged and still covered by its test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel
vercel Bot temporarily deployed to Preview – api-client-nextjs October 21, 2026 12:14 Inactive
@vercel
vercel Bot temporarily deployed to Preview – api-client-react-vite October 21, 2026 12:14 Inactive

@ehsan-platas ehsan-platas left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

This branch was previously deployed

2 inactive deployments
Preview – api-client-react-vite — 09d2e778 Deployed Oct 21, 2026 by vercel[bot]
Preview – api-client-nextjs — 09d2e778 Deployed Oct 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RPC bridge (createRpcRouteHandler/createRpcHandler) has no non-JSON response support (file downloads, binary)

2 participants