Skip to content

Angular SSR redirects can take healthy proxies upstreams out of rotation through an unbounded Location header #34365

Closed as not planned

Description

@SkyZeroZx

Description

createRedirectResponse() sets Location from request data without any length limit:

resHeaders.set("Location", location);

Route redirects copy request path segments into a relative redirectTo, and render redirects send the Router's final URL, query included, when it differs from the request (#31497). Both are intended, but nothing bounds the size. The header is at least as large as the request, and percent-encoding on the way makes it bigger: new URL() encodes " ' < > and the Router's query serializer encodes characters like / = ? |, so it is about 3x a raw request, and about 5x when a guard puts state.url in returnUrl and the % is encoded again. Both encodings are correct.

Behind nginx this affects other users. The response doesn't fit in nginx's default 4k proxy_buffer_size, so nginx answers 502 and counts it as a failed attempt for that backend (max_fails=1, fail_timeout=10s). With two backends and one worker, two unauthenticated GETs leave the upstream empty and every request gets 502 for about ten seconds, while both Node processes are fine.

I think this is bit similar as unpoly-rails GHSA-m875-3xf6-mf78.

A buffer sized for the app's largest redirect avoids it, but nothing tells the operator that size and it changes when someone adds a guard: for the plain returnUrl guard at nginx's 8 KB request line it is about 40 KB. max_fails=0 also avoids it, by turning passive health checks off.

The app doesn't need any redirect. With the server routes on RenderMode.Server, as in the SSR guide, this request gets a 302 with a 4,208 B Location:

GET /home/(x)?q=<1,400 raw ">     1,412 B

The ** route drops the unmatched (x) group, so the Router ends on /home?q=.... In the browser that is a replaceState; on the server #31497 turns it into a 302. Nothing in the Router needs to change.

Minimal Reproduction

// app.routes.ts
export const routes: Routes = [
  { path: "", component: Home },
  { path: "home", component: Home },
  { path: "**", component: NotFound },
];

// app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
  { path: "**", renderMode: RenderMode.Server },
];
ng build
NG_ALLOWED_HOSTS=localhost PORT=4000 node dist/app/server/server.mjs &
NG_ALLOWED_HOSTS=localhost PORT=4001 node dist/app/server/server.mjs &

nginx with one worker and nothing else tuned:

worker_processes 1;
events {}
http {
  upstream ssr {
    server 127.0.0.1:4000;
    server 127.0.0.1:4001;
  }
  server {
    listen 8080;
    location / {
      proxy_pass http://ssr;
      proxy_set_header Host localhost;
    }
  }
}

The request has to go out raw, fetch and node:http percent-encode the quotes first. I sent one per second for 25 s while normal traffic hit /home, and counted until /home answered 200 again:

Setup /home OK
nginx, the config above 15.2%
nginx, Compose service with two replicas 9.5%
nginx image as shipped on 2 vCPUs (two workers) 25.2%
nginx, worker_processes auto with a shared zone 9.3%
OpenResty, plain upstream 4.8%
Kong 3.7, passive checks with 502 in unhealthy.http_statuses 1.3%
nginx, one backend 100%
Same setups without those requests 100%

I also reproduced it on a VPS with nginx behind Cloudflare.

See https://github.com/SkyZeroZx/angular-ssr-location-lab

Exception or Error

[error] upstream sent too big header while reading response header from upstream, request: "GET /home/(x)?q=..."
[warn] upstream server temporarily disabled while reading response header from upstream
[error] no live upstreams while connecting to upstream, request: "GET /home HTTP/1.1"

Your Environment

Angular CLI       22.2.2
@angular/ssr      22.2.2
@angular/core     22.2.2
@angular/router   22.2.2
Node.js           22.23.3
Build             production
Engine            AngularNodeAppEngine
Render mode       RenderMode.Server
nginx             1.27.5, 1.29.3, 1.30.4 and 1.31.2

Activity

  1. added this to the needsTriage milestone on Oct 15, 2026
  2. alan-agius4 commented on Oct 16, 2026

    @alan-agius4
    Collaborator

    Closing as per #34366 (comment).

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions