In an Angular SSR app, how do you make the not-found page answer HTTP 404, and when is inject(RESPONSE_INIT) null?
answer
- static vs render-time status
- ServerRoute status field
- tokens from @angular/core
- only RenderMode.Server provides them
basics
~20 sEither 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 sWith `@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 linesimport { 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
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.
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.
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.
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