Practical technical guides / Security and HTTP

Redirects That Carry Secrets: What Clients Strip on a New Host and What 307 and 308 Resend

Fetch and requests strip Authorization on a new host, but keep custom headers and resend the body on 307 and 308. A loopback reproduction and rules for token requests.

By Sandeep Belgavi · Published 2026-11-12

Fetch and requests strip Authorization on a new host, but keep custom headers and resend the body on 307 and 308. A loopback reproduction and rules for token requests.

A refresh token sent to the wrong host

An OAuth client refreshes its access token by sending a POST to the authorization server's token endpoint. The refresh token is in the form body, and often the client secret is too. If that endpoint answers with a redirect to another host, and the client follows it, the question is simple and the answer is often surprising: which of those secrets reach the new host?

This came up in public on 9 November 2026, when the Claude Code 2.1.321 changelog listed a fix for "MCP OAuth requests sending secrets such as refresh tokens to another host when a server redirected them". That line is the only public detail, and the tool is not open source, so this guide does not describe how its code worked or how it was fixed. It uses the line as a prompt to examine the general mechanism with primary sources and a reproduction you can run: the HTTP status code rules, the Fetch standard's redirect algorithm, the Python requests library's redirect code, and two recorded runs on a loopback test server.

What the redirect status codes require

RFC 9110 defines five redirect codes that clients follow automatically. For 301 and 302, it notes that for historical reasons a user agent may change the request method from POST to GET for the subsequent request. For 303 the client is expected to retrieve the target with GET. For 307 and 308 the client must not change the request method; the whole point of those codes is that the same request, method and body included, is repeated at the new location.

The OAuth 2.0 Security Best Current Practice, RFC 9700, discusses the consequence for one direction of this in its section on 307 redirects. An authorization server that redirects a user agent after the user submitted credentials in a POST must not use 307, because the user agent will re-send the credentials in the body to the redirect target. It recommends 303. The same mechanics apply to any client that posts secrets in a body and follows a redirect, including a token endpoint client.

What clients strip, and what they do not

Header handling is a separate rule from body handling, and most clients only implement the first.

The WHATWG Fetch standard, which browsers and Node.js's built-in fetch follow, defines the redirect step. When the redirect is a 301 or 302 for a POST, or a 303 for anything other than GET or HEAD, the method becomes GET and the body is set to null. Separately, when the new location's origin differs from the current one, the algorithm deletes the CORS non-wildcard request-header names from the request; the standard's own note says this means the Authorization header is removed the moment another origin is seen. Nothing in that step removes custom headers such as X-Api-Key, and nothing removes the body for 307 or 308.

The Python requests library behaves the same way in practice, with its own rules for when the host has changed. In its SessionRedirectMixin (the logic is identical in release v2.34.2), rebuild_method turns 303 into GET, turns 302 into GET, and turns a POST answered with 301 into GET. For every status other than 307 and 308, it drops the body and the Content-Length, Content-Type and Transfer-Encoding headers. should_strip_auth removes Authorization when the hostname changes, and also when the port or scheme changes, with one exception kept for compatibility: a redirect from http to https on the default ports keeps it. The Cookie header is removed and rebuilt from the session's cookie jar. Custom headers stay.

The reproduction

The companion starts two loopback servers. The first, on 127.0.0.1, plays the token endpoint and answers every POST with a redirect to the second, on localhost. Because the hostname differs, the second server is a different origin for Fetch and a different host for requests. The second server records what it received. The client sends one POST with three synthetic secrets: a bearer token in Authorization, an API key in X-Api-Key, and a form body containing refresh_token and client_secret.

Recorded results for requests 2.33.1 and for Node.js 22.17.0's built-in fetch were identical:

status  method at new host  Authorization  X-Api-Key  refresh token in body
301     GET                 removed        sent       no
302     GET                 removed        sent       no
303     GET                 removed        sent       no
307     POST                removed        sent       yes
308     POST                removed        sent       yes

Two lessons follow. The Authorization header is handled correctly by both clients. Everything else is not their concern: an API key in a custom header goes to the new host for every status, and a refresh token in the body goes there on 307 and 308. A client that puts its credentials in the body, which is how RFC 6749 describes refresh requests and one of the ways it allows clients to authenticate, is relying on the token endpoint never answering 307 or 308 to somewhere else.

A hand-written retry loop that copies the whole request to the Location URL is worse than both: it forwards everything, Authorization included, for every status. The companion's model includes that case because it is what many small HTTP helpers and agent tool wrappers do.

Why agents and tool clients are exposed

A tool client in an agent host talks to servers chosen by configuration, sometimes by users, and those servers' URLs change: an MCP server moves behind a new gateway, an authorization server adds a regional endpoint, a misconfigured proxy answers with a redirect. Agents also make more of these calls than a person does, unattended, and their HTTP plumbing is often a thin wrapper written for convenience, with redirect following turned on because it made a demo work.

The safe design treats credential-bearing requests as a different class from ordinary fetches.

Rule one: do not follow redirects on credential-bearing requests

For token requests, refresh requests, and any request carrying a secret in the body or in a custom header, turn automatic redirects off and treat a 3xx as an error to investigate. In requests that is allow_redirects=False; in fetch it is redirect: "error" or "manual".

import requests

def refresh(token_url, refresh_token, client_id, client_secret):
    resp = requests.post(
        token_url,
        data={"grant_type": "refresh_token", "refresh_token": refresh_token},
        auth=(client_id, client_secret),
        allow_redirects=False,
        timeout=10,
    )
    if 300 <= resp.status_code < 400:
        raise RuntimeError(f"token endpoint redirected to {resp.headers.get('Location')!r}; refusing to follow")
    resp.raise_for_status()
    return resp.json()

Sending the client secret with HTTP Basic authentication instead of in the body also helps: Authorization is the one place both clients strip on a host change. It does not protect the refresh token itself, which RFC 6749 puts in the body.

Rule two: if you must follow, follow only within an allow-list

Some deployments legitimately redirect, for example from a bare host to a versioned path. If a redirect has to be followed, do it manually: check that the new location is the same origin, or an origin on an explicit allow-list for that credential, and only then re-send. Re-derive the secrets for the new request rather than copying headers, so a credential is only ever attached to the origin it belongs to.

Rule three: put credentials where clients know to strip them

If you control the API design, prefer the Authorization header over custom headers for credentials. Fetch and requests both know Authorization is sensitive. Neither knows that X-Api-Key, Private-Token or any other custom name is. If you consume an API that requires a custom header, your client code has to strip it on redirects itself.

Verifying your clients

  • Run the loopback probe against each HTTP client your services and agent tools use, including wrappers and SDKs, not only the underlying library.
  • For each credential-bearing call, assert in a test that redirects are disabled or allow-listed.
  • Log and alert on any 3xx response from token endpoints and other credential-bearing endpoints; they should not happen.
  • Review proxy and gateway rules in front of authorization servers for 307 or 308 redirects to other hosts.

The model in the interactive lab covers one hop, the two client behaviours above, a copy-everything loop and a no-follow client. It does not model cookies, proxies, multi-hop chains or every client library, so treat it as a way to reason about cases and the probe as the way to check your own.

Run the example yourself

The loopback probe for requests and Node.js fetch, the recorded results, and the offline model behind the lab with tests that it agrees with the recording. Companion code on GitHub; its README lists the prerequisites, exact commands, expected output and limits.

Interactive experiment

Redirect secret forwarding: which credentials reach the next host. Pick a client, a redirect status and an origin change, and see which secrets the next host receives: Authorization, a custom key header or the request body. Synthetic inputs; read its assumptions and limits before drawing conclusions.

Primary references and authorship

Sources read 2026-11-12. Original explanation and synthetic examples by Sandeep Belgavi. Views are my own and do not represent any employer, past or present.