Skip to content

fix: preserve HTTPS scheme in trailing-slash redirects - #3204

Open
Nicknamess96 wants to merge 1 commit into
letta-ai:mainfrom
Nicknamess96:fix/https-redirect-trailing-slash
Open

fix: preserve HTTPS scheme in trailing-slash redirects#3204
Nicknamess96 wants to merge 1 commit into
letta-ai:mainfrom
Nicknamess96:fix/https-redirect-trailing-slash

Conversation

@Nicknamess96

@Nicknamess96 Nicknamess96 commented Feb 25, 2026

Copy link
Copy Markdown

Summary

Enable uvicorn's proxy header support so that X-Forwarded-Proto from reverse proxies is respected when constructing trailing-slash redirect Location headers, preventing unintended HTTPS-to-HTTP protocol downgrades.

Problem

When Letta is deployed behind an HTTPS-terminating reverse proxy (Cloudflare Tunnels, nginx, Traefik, AWS ALB, etc.), endpoints that trigger trailing-slash redirects generate Location: http://... URLs instead of Location: https://.... Per the Fetch specification, HTTP clients strip Authorization headers on HTTPS-to-HTTP redirects, resulting in 401 Unauthorized errors.

Root cause: uvicorn.run() in start_server() is called without proxy_headers=True, so Starlette doesn't read X-Forwarded-Proto when building redirect URLs.

Changes

  • Add proxy_headers=True and forwarded_allow_ips="*" to both uvicorn.run() call sites (HTTP and LOCAL_HTTPS modes) in letta/server/rest_api/app.py
  • Add tests verifying redirect scheme preservation with uvicorn's ProxyHeadersMiddleware
  • Add tests verifying uvicorn.run() receives the correct proxy parameters

Testing

  • 7 new tests in tests/test_proxy_headers.py, all passing
  • Formatted with ruff format (line length 140)
  • Linted with ruff check (all checks passed)

Fixes #3189

Enable uvicorn's proxy header support so that X-Forwarded-Proto from
reverse proxies (Cloudflare Tunnels, nginx, Traefik, etc.) is respected
when constructing redirect Location headers. Without this, trailing-slash
redirects downgrade HTTPS to HTTP, causing clients that follow the Fetch
spec to strip Authorization headers on the redirect.

- Add proxy_headers=True and forwarded_allow_ips="*" to both uvicorn.run
  call sites (HTTP and LOCAL_HTTPS modes)
- Add tests verifying redirect scheme preservation with ProxyHeadersMiddleware
- Add tests verifying uvicorn.run receives correct proxy parameters

Fixes letta-ai#3189
@Nicknamess96

Copy link
Copy Markdown
Author

Friendly ping — this is a one-line fix to preserve the HTTPS scheme when the trailing-slash redirect middleware rewrites URLs. Happy to address any feedback.

@alvinttang alvinttang 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.

Code Review

Security: forwarded_allow_ips="*" trusts all proxy headers unconditionally

File: letta/server/rest_api/app.py, both uvicorn.run() call sites

proxy_headers=True,
forwarded_allow_ips="*",

Setting forwarded_allow_ips="*" means any client can set X-Forwarded-Proto, X-Forwarded-For, etc., and uvicorn will trust them. This is a well-known security anti-pattern:

  1. IP spoofing: An attacker can set X-Forwarded-For: 127.0.0.1 to bypass IP-based access controls or rate limiting that relies on the client IP from the ASGI scope.
  2. Scheme spoofing: An attacker can set X-Forwarded-Proto: https to trick the application into thinking it's behind HTTPS when it isn't, potentially bypassing secure-cookie checks.

For deployments behind a known reverse proxy, forwarded_allow_ips should be set to the proxy's IP address(es) or subnet, not "*". If the deployment topology varies, this should at minimum be configurable via an environment variable with a secure default:

forwarded_allow_ips=os.environ.get("LETTA_FORWARDED_ALLOW_IPS", "127.0.0.1"),

This way the default is secure (only trust localhost as a proxy), and operators behind Cloudflare/nginx can explicitly opt in to broader trust.

Minor: Tests don't test the actual app.py middleware chain

The tests in test_proxy_headers.py build a separate minimal FastAPI app and manually wrap it with ProxyHeadersMiddleware. This validates that uvicorn's middleware works correctly (which is uvicorn's responsibility), but doesn't test that the actual Letta application's middleware stack, route definitions, and startup sequence correctly integrate the proxy headers configuration. Consider adding an integration test that imports the real create_app() and verifies the middleware is present.

@themavik themavik 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.

Enabling forwarded_allow_ips="*" means any client that can reach the socket can forge X-Forwarded-Proto/Host; fine if Letta only listens on loopback or behind a locked-down proxy hop, but worth calling out in docs or tightening to known proxy CIDRs for public binds.

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.

Trailing-slash redirects use HTTP scheme behind HTTPS proxies, breaking Authorization forwarding

3 participants