skip to content

In an Angular SSR app, how do you make the not-found page answer HTTP 404, and when is inject(RESPONSE_INIT) null?

level: middleimportance: should knowfreq 45%

answer

  1. static vs render-time status
  2. ServerRoute status field
  3. tokens from @angular/core
  4. only RenderMode.Server provides them

basics

~20 s

Either give the server route a static status: 404, or inject RESPONSE_INIT in the not-found component and set status to 404. RESPONSE_INIT, like REQUEST, is null in the browser, when prerendering, and for Client routes.

solid answer

~40 s

With `@angular/ssr` a `ServerRoute` can declare `status` and `headers`, so `{ path: '**', renderMode: RenderMode.Server, status: 404 }` works when only unknown URLs reach that entry. The more robust way is to set it at render time: the not-found component calls `inject(RESPONSE_INIT)` from `@angular/core` and, if the result is not null, sets `status = 404`; the engine builds the final `Response` from that object after rendering. `REQUEST`, `RESPONSE_INIT` and `REQUEST_CONTEXT` are provided only for `RenderMode.Server` renders. They are `null` in the browser, during the build and prerendering, for `RenderMode.Client` routes and during development route extraction, so every use needs a null check, and a prerendered page cannot set its status this way.

code

ts · 14 lines
ts
import { Component, inject, RESPONSE_INIT } from '@angular/core';

@Component({
  selector: 'app-not-found',
  template: `<h1>Page not found</h1>`,
})
export class NotFound {
  constructor() {
    const init = inject(RESPONSE_INIT);
    if (init) {
      init.status = 404;
    }
  }
}

go deeper

for a junior

Recall that a not-found page answers 200 unless you change it, and that Angular gives you a RESPONSE_INIT token to set the status.

for a middle

Explain the static status field versus the RESPONSE_INIT token, why the token can be null, and why that null check is mandatory in shared components.

for a senior

Spot the trap of putting status: 404 on a catch-all that real routes depend on, and choose render-time status for pages that discover missing data.

for a principal

Treat status codes as part of the rendering contract per route, and make sure the chosen render mode can actually produce the codes crawlers and caches rely on.

## Where the HTTP status of an SSR page comes from When `@angular/ssr` renders a route with **`RenderMode.Server`**, the engine builds a `ResponseInit` object *before* rendering, starting from the matched `ServerRoute`'s `status` and `headers` (plus a `Content-Type` of `text/html;charset=UTF-8`). It renders the app, then creates the final `Response` from that same object. Anything that changes the object during rendering therefore changes the response. A not-found page is usually the Router's wildcard route (`{ path: '**', component: NotFound }`). Without extra work it answers **200**, which tells crawlers the URL is a real page. There are two ways to fix that. ## Option 1: a static status on the server route `ServerRoute` accepts `status` and `headers` for `Server` and `Client` entries: ```ts { path: '**', renderMode: RenderMode.Server, status: 404 } ``` This is simple but only correct if the `**` server entry is reached **only** by unknown URLs. If real pages such as `product/:id` have no server entry of their own and rely on `**` too, they will also answer 404. Also note: - The `ServerRoutePrerender` type omits `status`, so a prerendered entry cannot declare one; a prerendered page is served as a stored file. - A `status` on a route that redirects must be a redirect code (301, 302, 303, 307 or 308); the build reports anything else. ## Option 2: set it at render time with RESPONSE_INIT `@angular/core` exports three **injection tokens** for server rendering: | Token | Type | What it gives you | | --- | --- | --- | | `REQUEST` | Web `Request` or `null` | The incoming request: URL, headers, cookies | | `RESPONSE_INIT` | `ResponseInit` or `null` | The mutable status and headers of the outgoing response | | `REQUEST_CONTEXT` | `unknown` | Whatever the server passed as the second argument of `handle()` | The not-found component can then decide for itself: ```ts import { Component, inject, RESPONSE_INIT } from '@angular/core'; @Component({ selector: 'app-not-found', template: `<h1>Page not found</h1>`, }) export class NotFound { constructor() { const init = inject(RESPONSE_INIT); if (init) { init.status = 404; } } } ``` This works whatever the server route list looks like, and it lets other components set a status when they discover, while rendering, that a record does not exist. ## When the tokens are null All three tokens are declared with `providedIn: 'platform'` and a factory that returns `null`. The engine overrides them with real values **only when the matched route's mode is `RenderMode.Server`**. They are `null`: 1. in the browser, including after hydration; 2. while building and prerendering (`RenderMode.Prerender`), because no request exists; 3. for `RenderMode.Client` routes, because Angular does not render them on the server; 4. during route extraction in development, at the time of the request. That is why the `if (init)` guard is not optional: the same component runs in the browser, where `inject(RESPONSE_INIT)` returns `null`. ## Reading the request `inject(REQUEST)` is the supported way for a server-rendered page to read cookies or headers, for example `inject(REQUEST)?.headers.get('accept-language')`. It is `null` in the same situations, so code must handle both. A page whose output depends on `REQUEST` belongs in `RenderMode.Server`; putting it in `Prerender` silently renders the "no request" branch once for everyone. ## Summary of the choice - Use a **static `status`** when a whole server route always has the same code. - Use **`RESPONSE_INIT`** when the code depends on data found during rendering. - Remember that **neither applies to a prerendered page**, which is served as a stored file. ## Mistakes interviewers listen for - Writing `inject(RESPONSE_INIT)!.status = 404` without a guard: the same component runs in the browser, where the token is `null`, and the assignment throws. - Expecting a status set in a `RenderMode.Prerender` or `RenderMode.Client` route to reach the client; neither render path provides the token. - Adding `status: 404` to the `**` server entry while real parameterised pages still fall through to it. - Assuming the Router's wildcard route changes the HTTP status on its own; it only chooses which component renders.

  • Why can a prerendered not-found page not return 404 this way?
    At build time there is no request, so `RESPONSE_INIT` is `null` and the guard skips the assignment. The page is stored as a file and served as such, and the `ServerRoutePrerender` type does not accept a `status` field either.
  • How would a server-rendered page read the user's preferred language?
    Inject `REQUEST` from `@angular/core` and read `request?.headers.get('accept-language')`. The token is a Web `Request`, so headers and cookies come from its `headers` object, and the optional chaining covers the browser and prerender cases where it is `null`.
  • What status codes can a redirecting server route declare?
    Only redirect codes: 301, 302, 303, 307 or 308. Route extraction reports any other `status` on a route with `redirectTo`, and a server-side redirect without one uses 302.

saying these in an interview costs you the question

  • The Router's wildcard route makes Angular SSR answer 404 automatically
  • inject(RESPONSE_INIT) always returns an object on the server, including when prerendering
  • status: 404 on the ** server route is safe even when real pages rely on that entry
  • Setting RESPONSE_INIT in the browser changes the page's HTTP status
  • A prerendered route can declare status in its ServerRoute entry