Skip to content

Preview URLs: Authorization header silently rewritten by edge regardless of basicAuth/bearerToken config #167

Description

@shtefcs

Problem

Public preview URLs are documented as supporting three auth modes via box.getPublicURL(port, options):

  1. Default (no options): public URL, no auth.
  2. { bearerToken: true }: Upstash issues a Bearer token.
  3. { basicAuth: true }: Upstash issues username + password.

When mode 1 (default) is used, the URL is documented as fully public with no Authorization enforcement. In practice, Upstash's preview edge silently overwrites whatever Authorization header the client sends and replaces it with a Basic auth header before the request reaches the box's dev server.

Repro

Create a box, expose a port with no auth options:

const box = await Box.create({ ... });
await box.getPublicURL(3000);  // no options

Inside the box, run a Next.js dev server that echoes request headers. Then from outside:

curl -H "Authorization: Bearer my-jwt-token" https://<box>-3000.preview.box.upstash.com/api/echo-headers

Expected: the dev server sees Authorization: Bearer my-jwt-token.

Actual: the dev server sees Authorization: Basic admin:<64-char-hex>. The original client header is gone. The injected Basic value is the same on every request for a given box, suggesting it is a per-box internal proxy credential rather than dynamic.

Same behavior with no Authorization header at all on the client side:

curl https://<box>-3000.preview.box.upstash.com/api/echo-headers

The dev server still sees Authorization: Basic admin:<hex>.

Confirmation that the URL has no auth configured

Listing the box's public URLs via the SDK shows no token, username, or password fields:

curl -H "Authorization: Bearer $UPSTASH_BOX_API_KEY" \
  https://<region>.box.upstash.com/v2/box/<box-id>/preview

Returns:

{
  "previews": [
    {
      "id": "<box-id>-3000",
      "box_id": "<box-id>",
      "port": 3000,
      "created_at": <ts>
    }
  ]
}

No bearer_token, no basic_auth. So this is not the expected behavior of any documented auth mode.

Impact

Any user app that uses the industry-standard Authorization: Bearer <jwt> pattern for its own auth (Auth.js, NextAuth, JWT-based sessions, custom JWT routes, etc.) works correctly when deployed to Vercel / Cloudflare Pages / anywhere else, but returns 401 on every authenticated request when viewed inside the preview iframe. The JWT never reaches the app's verifyToken because Upstash has already replaced the header with its own Basic credential.

This breaks AI coding agent platforms that build apps and let users test them in a sandbox preview. The "build a TODO app with login" flow works end to end on the deployed site but fails to load any data in the preview because every fetch returns 401. Users report this as a platform bug when it is actually Upstash silently mangling the header.

We hit this in production on Automatio and chased it through the wrong layers (our CF Worker proxy, env vars, AUTH_SECRET) for hours before identifying Upstash as the source. We are filing it as a separate issue from #101 because the cause is different (header rewrite vs same-origin / CORS / WebSocket gaps), and the workaround is different too (change app code, not iframe / proxy config).

Workarounds we are aware of

These all require changes inside the user app:

  • Send the token via a custom header like X-Auth-Token and read it from there in backend routes. Upstash leaves X-Auth-Token alone.
  • Send the token via a cookie (Set-Cookie on login response, req.cookies on backend). Cookies pass through Upstash untouched.

We do not want to force our coding agent to generate non-standard auth code (the Authorization: Bearer pattern is the right default for the deployed world), so we are blocked on this.

Asks

  1. Confirm whether the Basic header injection is intentional behavior for default public URLs. If yes, please document it so other platforms do not hit the same trap.
  2. If it is intentional, please consider adding an option to disable the rewrite for public URLs that opted out of auth.
  3. If it is unintentional, this is a bug worth fixing. The expected behavior for getPublicURL(port) with no auth options is that the dev server sees exactly what the client sent.

Environment

  • @upstash/box SDK current version
  • Verified on multiple boxes in multiple regions

Related: this is from the same "Upstash edge interferes with user app headers" family as #101 (origin and CORS) and #106 (x-forwarded-host being internal IP).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions