Skip to content

ssr: string redirectTo drops trailing slash in query and fragment #34511

Open

Description

@thekhegay

Command

build, serve

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

none, same baseHref join is there since 19.0.0

Description

String redirectTo loses trailing / of query or fragment in Location: '/login?next=/a/' gives /login?next=/a and '/login#section/' gives /login#section. Same with X-Forwarded-Prefix and with base href /base/, on 22.2.4 and on main snapshot 22.3.0-next.3+sha-928169a

Function redirect with same target keeps it (/login?next=%2Fa%2F and /login#section/) since router serializes it, so same target gives diffrent Location depending only on how redirect is written

Minimal Reproduction

  1. npx -p @angular/cli@latest ng new repro --ssr --defaults --skip-git
  2. src/app/app.routes.ts:
import { Component } from '@angular/core';
import { Routes } from '@angular/router';

@Component({ template: 'login' })
class Login {}

export const routes: Routes = [
  { path: 'login', component: Login },
  { path: 'old', redirectTo: '/login?next=/a/' },
  { path: 'old-hash', redirectTo: '/login#section/' },
  { path: 'old-fn', redirectTo: () => '/login?next=/a/' },
  { path: 'old-fn-hash', redirectTo: () => '/login#section/' },
];
  1. src/app/app.routes.server.ts: [{ path: '**', renderMode: RenderMode.Server }] so function redirects go through render
  2. ng build then NG_ALLOWED_HOSTS=localhost NG_TRUST_PROXY_HEADERS=x-forwarded-prefix node dist/repro/server/server.mjs
  3. curl:
$ curl -si localhost:4000/old | grep -i '^location'
location: /login?next=/a
$ curl -si localhost:4000/old-hash | grep -i '^location'
location: /login#section
$ curl -si localhost:4000/old-fn | grep -i '^location'
location: /login?next=%2Fa%2F
$ curl -si localhost:4000/old-fn-hash | grep -i '^location'
location: /login#section/
$ curl -si -H 'X-Forwarded-Prefix: /app' localhost:4000/old | grep -i '^location'
location: /app/login?next=/a
$ curl -si -H 'X-Forwarded-Prefix: /app' localhost:4000/old-hash | grep -i '^location'
location: /app/login#section

With ng build --base-href /base/ it's /base/login?next=/a and /base/login#section. Slash is gone already at build time, dist/repro/server/angular-app-manifest.mjs has "redirectTo": "/login?next=/a"

Expected: /login?next=/a/ and /login#section/, with /app or /base in front when set

Actual: trailing slash of query and fragment is gone

Exception or Error

none, only Location header is wrong

Your Environment

Angular CLI       : 22.2.4
Angular           : 22.2.3
Node.js           : 24.16.0
Package Manager   : npm 11.13.0
Operating System  : darwin arm64

┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package                   │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build            │ 22.2.4            │ ^22.2.4           │
│ @angular/cli              │ 22.2.4            │ ^22.2.4           │
│ @angular/common           │ 22.2.3            │ ^22.2.0           │
│ @angular/compiler         │ 22.2.3            │ ^22.2.0           │
│ @angular/compiler-cli     │ 22.2.3            │ ^22.2.0           │
│ @angular/core             │ 22.2.3            │ ^22.2.0           │
│ @angular/forms            │ 22.2.3            │ ^22.2.0           │
│ @angular/platform-browser │ 22.2.3            │ ^22.2.0           │
│ @angular/platform-server  │ 22.2.3            │ ^22.2.0           │
│ @angular/router           │ 22.2.3            │ ^22.2.0           │
│ @angular/ssr              │ 22.2.4            │ ^22.2.4           │
│ rxjs                      │ 7.8.2             │ ~7.8.0            │
│ typescript                │ 6.0.3             │ ~6.0.2            │
│ vitest                    │ 5.0.5             │ ^5.0.0            │
└───────────────────────────┴───────────────────┴───────────────────┘

same output with @angular/ssr 22.3.0-next.3+sha-928169a from angular-ssr-builds

Anything else relevant?

joinUrlParts trims trailing / of every part, and two places still pass it whole target with query and fragment: joinUrlParts(baseHref, metadata.redirectTo) in extractRoutesAndCreateRouteTree and prefix join in AngularServerApp.handle

#34467 description says joinUrlParts is applied only to path portion when prepending baseHref or X-Forwarded-Prefix so trailing slashes in query or fragment are preserved, but both joins above still take whole target. If that's not intended I can send PR that splits off query and fragment first, like resolveRedirectTo does

Activity

  1. added this to the needsTriage milestone on Oct 23, 2026
  2. added a commit that references this issue on Oct 23, 2026
    a77eadd
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