Skip to content

fix: don't follow redirects on API requests - #1752

Open
nicknisi wants to merge 3 commits into
mainfrom
fix/no-follow-api-redirects
Open

nicknisi wants to merge 3 commits into
mainfrom
fix/no-follow-api-redirects

Conversation

@nicknisi

@nicknisi nicknisi commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Description

API requests follow redirects today (fetch's default redirect: 'follow'). authenticateWithCode and authenticateWithRefreshToken send client_secret in the request body, so if the API origin (or a configured apiHostname) ever answers with a 307/308, the SDK re-posts the body, secret included, to wherever the redirect points. The Authorization header is dropped cross-origin; the body is not. GET responses from the redirect target are also accepted as if they came from the API. Reported against authkit-nextjs in workos/authkit-nextjs#510.

Changes

  • FetchHttpClient sets redirect: 'manual', so redirects are never followed. I used 'manual' rather than 'error' because Cloudflare workerd rejects 'error' ("Invalid redirect value..."), and 'error' rejects with a TypeError, which shouldRetryRequest retries.
  • A 3xx (or a browser opaqueredirect) throws an HttpClientError that names the status and Location, so callers get a GenericServerException like WorkOS API responded with a redirect (301 to https://...). Redirects are not followed; check the apiHostname, port and https options. Without this, the HTML redirect body would be parsed as JSON and fail with a confusing ParseError. 3xx isn't in RETRY_STATUS_CODES, so it fails right away without retrying.
  • The base WorkOS class (used by createWorkOS) now passes options.fetchFn to FetchHttpClient. It silently ignored it before.
  • I removed WorkOSWorker.createHttpClient (used under the workerd / edge-light conditions). It was a copy of the base method minus timeout, and it also ignored fetchFn. The worker build now uses the base method, so both fetchFn and timeout work on Edge and Workers.

Verification

  • New real-HTTP test (native fetch and node-fetch): a 307 to /stolen isn't followed, the error names the redirect, and the request count stays at 1.

  • New tests: a 301 surfaces as a GenericServerException naming the redirect; the worker client and createWorkOS use the provided fetchFn; the worker client respects timeout. All of them fail without the source changes.

  • Repro against the built lib/ with a fake API returning a cross-origin 307 and a sink that records requests:

    Build Before After
    index.mjs (Node) sink receives client_secret nothing reaches the sink; redirect error in ~25ms
    index.worker.mjs on Node sink receives client_secret nothing reaches the sink
    index.worker.mjs on workerd (wrangler dev) sink receives client_secret nothing reaches the sink; custom fetchFn is called
  • npm test, npm run typecheck, npm run prettier and npm run build (attw) pass.

Compatibility: no change against the real API, which doesn't redirect. Setups that point apiHostname at something that answers with a redirect now get an error that says so, instead of having the request silently re-sent. One real case: https: false against api.workos.com, which answers 301 to https. GET calls used to follow that; now they fail with the redirect error.

Note: the existing GH-1679 real-HTTP timeout tests (times out a successful JSON body..., surfaces a stalled error body...) flake locally on main as well. They use a 100ms timeout. The new redirect test uses the default timeout so it doesn't hit the same problem.

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] Yes

authenticateWithCode and authenticateWithRefreshToken POST a body containing client_secret. With fetch's default redirect: 'follow', a 307/308 from the API origin re-posts that body to the redirect target (the Authorization header is dropped cross-origin, the body is not). Set redirect: 'manual' so a 3xx fails through the existing error path.

Also pass fetchFn through in the worker build and the base WorkOS class (used by createWorkOS). Both ignored it, so a custom fetch was silently dropped on Edge, Cloudflare Workers and createWorkOS clients.
@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; the previous connection-cleanup issue is fixed.

Summary

API requests now reject redirects instead of forwarding request bodies.

  • WorkOS requests stop at redirects and return a clear error.
  • Base and worker clients use the configured fetch function and timeout.

The previous finding is fixed. No new actionable issues were found. Tests were inspected but not run.

Reviews (3) · Last reviewed commit: "fix: release the connection on a redirec..." · Reviewed by Greptile

…lient

A 3xx (or a browser opaqueredirect) now throws an HttpClientError that names the status and Location, surfacing as a GenericServerException, instead of a ParseError from reading an HTML body as JSON.

WorkOSWorker's createHttpClient was a copy of the base class method minus timeout. Remove it so the worker build uses the base method, which also makes the timeout option work on Edge and Workers.
Comment thread src/common/net/fetch-client.ts Resolved
@nicknisi
nicknisi requested a review from gjtorikian November 4, 2026 00:02
The redirect branch threw with the response body still open. On Node
that pins the connection until garbage collection, and a proxy that
sends redirect headers but stalls the body would hold it indefinitely.
Abort the request's controller in a `finally` once the catch has already
checked the signal, so the abort releases the socket on every fetch
implementation (node-fetch bodies have no `cancel()`) without being
reported as a timeout. Reading the body instead would turn a stall into
a retried 408 that hides the redirect.

A browser reports a manual redirect as an opaque response with status 0
and no headers, which read as "redirect (0)". Say "an opaque redirect"
instead, and cover that branch with a test.

@gjtorikian gjtorikian left a comment

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.

LGTM~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants