skip to content

An Angular SSR news homepage still requests its headlines twice, on the server and again after hydration; how would you find out why the transfer cache missed?

level: seniorimportance: should knowfreq 38%

answer

  1. is there an ng-state entry?
  2. same key on both sides
  3. excluded by headers or credentials
  4. request after stability
  5. not HttpClient at all

basics

~20 s

Check whether the server stored the response in the ng-state script, then compare the browser request with the server one: a different origin or params, an auth header, credentials, uncacheable Cache-Control or Set-Cookie, a late request after stability, or a non-HttpClient call all cause misses.

solid answer

~40 s

First look at the page source: if the `ng-state` script has no entry for the headlines, the server never stored it, so the request was ineligible (auth header, credentials, `Cache-Control: no-store`/`no-cache`/`private`, `Set-Cookie` on the response, a `filter` or `transferCache: false`), was not made through `HttpClient`, or was never made on the server. If an entry exists but the browser still calls the network, the keys differ: the key hashes method, response type, URL, body and sorted params, so an internal server origin versus the public browser origin, or a cache-busting param such as a timestamp, causes a miss. Fix origins with `HTTP_TRANSFER_CACHE_ORIGIN_MAP` in the server config. Finally, check timing: a request made after the app is stable, say in a `setTimeout`, never consults the cache.

go deeper

for a junior

Know that a second request after hydration means the transfer cache did not store or match the server's response.

for a middle

List the default exclusions and the fields that make up the cache key, and check the ng-state script for evidence.

for a senior

Separate not stored, stored but mismatched, and requested too late; fix origins with the server-only map; treat relaxing exclusions as a security call.

for a principal

Make server and browser request paths identical by design, with shared URL builders and no cache busters, so misses cannot creep back in.

## The symptom A server-rendered news homepage shows its headlines immediately, but the browser's network panel shows a second `GET /api/headlines` right after hydration. The page may flicker if the list briefly resets. The **HTTP transfer cache** should have prevented that, so something breaks its contract: the server must store an eligible response, and the browser must make an **identical** request **before** the app becomes stable. ## Step 1: did the server store anything? Open the raw page source, not the live DOM, and find the `<script type="application/json">` whose id is the app id plus `-state` (`ng-state` by default). Keys are SHA-256 hashes, so look for the headline text in the values. If the tag is missing or holds no headline response, the server skipped it. Likely causes: - **Auth or cookies.** An interceptor adds `Authorization` on every request, or the server forwards the visitor's `Cookie`. Requests with `Authorization`, `Proxy-Authorization` or `Cookie` are excluded by default. - **Credentials.** `withCredentials: true`, or the Fetch `credentials` mode `include` or `same-origin`, excludes the request. - **Response headers.** The news API answers with `Cache-Control: no-cache`, `no-store` or `private`, or a load balancer adds `Set-Cookie`. Such responses are not stored. - **Explicit opt-outs.** A `filter` returning `false`, `transferCache: false`, or `withNoHttpTransferCache()` in the config. - **Not `HttpClient`.** A native `fetch()` or an SDK with its own transport never passes through Angular's interceptor. - **Not made on the server.** The call sits inside `afterNextRender()` or behind a browser-only check, so the server never ran it. - **No hydration.** `provideClientHydration()` is missing from the config the server and browser share, so the cache was never registered. ## Step 2: does the browser request match? If an entry exists and the browser still goes to the network, the **keys differ**. The key is a hash of the method, response type, URL, serialized body and query params. Param order is normalized, but values are not. Common mismatches: | Server request | Browser request | Why it misses | |---|---|---| | `http://api.internal:8080/headlines` | `https://news.example/api/headlines` | Different origin in the URL | | `?region=eu` | `?region=eu&t=1727...` | Cache-busting timestamp param | | `responseType: 'json'` | `responseType: 'text'` | Response type is part of the key | | `?limit=20` | `?limit=10` | Viewport-dependent params | For the origin case, provide `HTTP_TRANSFER_CACHE_ORIGIN_MAP` **in the server config only**. It maps the server origin to the browser origin before the key is computed. Angular can raise a runtime error when it finds the token in browser code, and in development mode it rejects map values that contain a path. ## Step 3: was it too late? In the browser, the cache is active only until `ApplicationRef` reports the app stable. A request fired after that point goes to the network by design: 1. a `setTimeout` or polling interval that starts the first load; 2. a request triggered by a user action; 3. a lazily created component whose data call comes after stability. ## Step 4: fix, then verify 1. Remove the cause: drop the auth header for the public headline endpoint, strip cache-busting params during SSR, fix the API's `Cache-Control` if it is wrong, or add the origin map. 2. If the headers are intentional, decide whether to relax a flag in `withHttpTransferCacheOptions()`. That is a security decision, not a performance tweak. 3. Reload and confirm that the network panel shows no headline request during hydration, and that the `ng-state` tag holds the entry. ```ts // app.config.server.ts import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; import { HTTP_TRANSFER_CACHE_ORIGIN_MAP } from '@angular/common/http'; import { provideServerRendering, withRoutes } from '@angular/ssr'; import { appConfig } from './app.config'; import { serverRoutes } from './app.routes.server'; const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(withRoutes(serverRoutes)), { provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP, useValue: { 'http://api.internal:8080': 'https://news.example' }, }, ], }; export const config = mergeApplicationConfig(appConfig, serverConfig); ``` ## A quick checklist 1. Is `provideClientHydration()` in the config both sides use, without `withNoHttpTransferCache()`? 2. Does the page source hold an entry with the headline data? 3. Does the request carry auth headers, cookies or credentials, on either side? 4. Does the API response carry `Cache-Control` no-store, no-cache or private, or `Set-Cookie`? 5. Are the method, URL (origin included), params, body and response type identical on both sides? 6. Is the browser request made before the app becomes stable? ## What a strong answer shows It separates "not stored" from "stored but not matched" from "matched too late", uses the page source as evidence instead of guessing, and knows that relaxing an exclusion flag has security consequences.

  • Why must HTTP_TRANSFER_CACHE_ORIGIN_MAP be provided only on the server?
    The map rewrites the server's request URL to the browser's origin before the cache key is computed, so both sides produce the same key. The browser already uses its own origin and needs no rewrite. The documented rule is server-only, and Angular can raise a runtime error when it detects the token in browser code, which catches configs merged into the wrong side.
  • The API sends Cache-Control: no-cache on the headlines. Should you set includeNonCacheableRequests?
    Only after confirming the header is a mistake for public, identical-for-everyone data. The flag also admits `private` responses and ones with `Set-Cookie`, app-wide. Fixing the API's header, or keeping the flag off and accepting one extra request, is usually safer than embedding data its owner marked uncacheable.

saying these in an interview costs you the question

  • HttpParams order breaks the cache key, so sort params by hand.
  • Any second request means the transfer cache is broken and should be replaced.
  • The origin map must be provided in both server and browser configs.
  • Relaxing includeRequestsWithAuthHeaders is a harmless performance fix.
  • The cache works for any fetch() call made during the server render.