Skip to content

Prerendered pages at non-ASCII addresses are built and then never served #33966

Description

@neolorn

Versions

Measured on:

Package Version
@angular/ssr 22.1.5
@angular/build 22.1.5
@angular/cli 22.1.5
@angular/core 22.1.3
@angular/router 22.1.3
TypeScript 6.0.3
Node.js 24.18.0

Summary

AngularAppEngine looks up its own prerendered assets using the request's raw, still
percent-encoded
pathname, while the build registers those assets under their decoded names.
For any prerendered route whose path contains a non-ASCII character, the lookup misses the file
the same build just produced, and the engine falls through to server-side rendering — or to 404,
depending on what the host does next.

Nothing is wrong with the file. It is on disk, at the right address, with the right content.

Reproduction

A prerendered route whose path contains a non-ASCII segment:

// app.routes.ts
export const routes: Routes = [
  { path: 'مقالات/:slug', component: ArticleRoute },
];

// app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
  {
    path: 'مقالات/:slug',
    renderMode: RenderMode.Prerender,
    getPrerenderParams: async () => [{ slug: 'دليل' }],
  },
  { path: '**', renderMode: RenderMode.Server },
];
ng build --configuration production
node dist/<app>/server/server.mjs
curl -i 'http://localhost:4000/%D9%85%D9%82%D8%A7%D9%84%D8%A7%D8%AA/%D8%AF%D9%84%D9%8A%D9%84'

A browser sends the same encoded form — it is what percent-encoding a non-ASCII path is — so
this is the ordinary request path, not a contrived one.

Expected

The prerendered file is served, with ng-server-context="ssg".

Actual

The prerendered asset is not found. The request is server-rendered instead, or 404s if no route
matches.

Measured on an application with four prerendered addresses, two per locale. Every ASCII address
returned 200 with ng-server-context="ssg". The two addresses with a non-ASCII segment did not
resolve to their prerendered files, with those files present on disk the whole time.

Mechanism

The build writes the asset and registers it under the decoded path. From the generated
angular-app-manifest.mjs:

ar-eg/articles/دليل-أطلس/index.html

At runtime the lookup key is built from the raw request pathname, with no decoding
(ssr.mjs:1376-1389):

buildServerAssetPathFromRequest(request) {
  let { pathname: assetPath } = new URL(request.url);
  if (!assetPath.endsWith('/index.html')) {
    assetPath = joinUrlParts(assetPath, 'index.html');
  }
  const { baseHref } = this.manifest;
  if (baseHref.length > 1 && assetPath.startsWith(baseHref)) {
    assetPath = assetPath.slice(baseHref.length);
  }
  return stripLeadingSlash(assetPath);
}

new URL(...).pathname preserves percent-encoding, so the key is

ar-eg/articles/%D8%AF%D9%84%D9%8A%D9%84-%D8%A3%D8%B7%D9%84%D8%B3/index.html

which does not match the registered name. handleServe then returns null at its
assets.hasServerAsset(assetPath) check (ssr.mjs:1224 calls the builder; the guard follows),
and handle continues to handleRendering.

The file's only decodeURIComponent is at ssr.mjs:407, in route-tree construction
(route.split('/').filter(Boolean).map(decodeURIComponent)) — the asset lookup has none. That
asymmetry is the defect: the route tree is built from decoded segments, the asset key is not.

Why it matters

Prerendering is requested for a reason — latency, cost, crawlability — and this silently
withdraws it for exactly the routes a localized application translates. There is no error and no
warning; the page still renders, just server-side every time, so the symptom is a performance and
cost regression that no build output mentions.

It is also invisible to a test suite that uses ASCII addresses, which is most of them. The
prerendered file exists, the address is correct, and the only thing wrong is which of two spellings
the lookup uses.

Suggested fix

Decode the pathname when building the asset key, so it matches the name the build registered —
per segment, to match how the route tree is already built at :407.

Metadata

Metadata

Assignees

No one assigned

    标签

    Type

    No type

    项目

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions