Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions adev/src/content/guide/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ To configure this, update your `angular.json` file as follows:
You can customize how Angular caches HTTP responses during server‑side rendering (SSR) and reuses them during hydration by configuring `HttpTransferCacheOptions`.
This configuration is provided globally using `withHttpTransferCacheOptions` inside `provideClientHydration()`.

By default, `HttpClient` caches all `HEAD` and `GET` requests which don't contain `Authorization`, `Proxy-Authorization`, or `Cookie` headers and are not sent with `withCredentials`. You can override those settings by using `withHttpTransferCacheOptions` to the hydration configuration.
By default, `HttpClient` caches all `HEAD` and `GET` requests which don't contain `Authorization`, `Proxy-Authorization`, or `Cookie` headers and are not sent with `withCredentials` or Fetch API `credentials` modes that can send credentials. Angular also skips transfer cache when a request or response includes `Cache-Control` directives that forbid caching (`no-store`, `no-cache`, or `private`), or when the Fetch API `cache` option is set to `no-store` or `no-cache`. You can override the request filtering settings by using `withHttpTransferCacheOptions` in the hydration configuration.

```ts
import { bootstrapApplication } from '@angular/platform-browser';
Expand Down Expand Up @@ -397,6 +397,8 @@ withHttpTransferCacheOptions({

IMPORTANT: Avoid including sensitive headers like authentication tokens. These can leak user‑specific data between requests.

Including `Cache-Control` in `includeHeaders` only makes that header available on the hydrated response. Angular already evaluates `Cache-Control` headers automatically when deciding whether a request or response is eligible for transfer cache.

---

### `includePostRequests`
Expand All @@ -417,7 +419,7 @@ Use this only when `POST` requests are **idempotent** and safe to reuse between
### `includeRequestsWithAuthHeaders`

Determines whether requests containing `Authorization`, `Proxy‑Authorization`, or `Cookie` headers are eligible for caching.
By default, these are excluded to prevent caching user‑specific responses. Requests sent with `withCredentials` are also excluded by default.
By default, these are excluded to prevent caching user‑specific responses. Requests sent with `withCredentials` or Fetch API `credentials` set to `include` or `same-origin` are also excluded by default.

```ts
withHttpTransferCacheOptions({
Expand Down Expand Up @@ -480,6 +482,10 @@ To disable caching for an individual request, you can specify the [`transferCach
httpClient.get('/api/sensitive-data', { transferCache: false });
```

`HttpTransferCache` does not cache requests or responses that explicitly opt out of caching. Angular skips transfer cache entries when a request includes a `Cache-Control` header with `no-store`, `no-cache`, or `private`, or when the request uses the Fetch API `cache` option set to `no-store` or `no-cache`. Responses with `Cache-Control: no-store`, `Cache-Control: no-cache`, or `Cache-Control: private` are also not stored in the transfer cache.

NOTE: If your application uses different HTTP origins to make API calls on the server and on the client, the `HTTP_TRANSFER_CACHE_ORIGIN_MAP` token allows you to establish a mapping between those origins, so that `HttpTransferCache` feature can recognize those requests as the same ones and reuse the data cached on the server during hydration on the client.

## Configuring a server

### Node.js
Expand Down
38 changes: 34 additions & 4 deletions packages/common/http/src/transfer_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ import {HttpParams} from './params';
* (for example using GraphQL).
* @param includeRequestsWithAuthHeaders Enables caching of requests containing `Authorization`,
* `Proxy-Authorization`, or `Cookie` headers. By default, these requests are excluded from
* caching. Requests sent using `withCredentials` are also excluded by default.
* caching. Requests sent using `withCredentials` or Fetch API `credentials` modes that can send
* credentials are also excluded by default.
*
* @see [Configuring the caching options](guide/ssr#configuring-the-caching-options)
*
Expand Down Expand Up @@ -136,12 +137,16 @@ export function transferCacheInterceptorFn(
!isCacheActive ||
requestOptions === false ||
// Do not cache requests sent with credentials.
req.withCredentials ||
hasOutgoingCredentials(req) ||
// POST requests are allowed either globally or at request level
(requestMethod === 'POST' && !globalOptions.includePostRequests && !requestOptions) ||
(requestMethod !== 'POST' && !ALLOWED_METHODS.includes(requestMethod)) ||
// Do not cache requests with authentication or cookie headers unless explicitly enabled.
(!globalOptions.includeRequestsWithAuthHeaders && hasAuthHeaders(req)) ||
// Do not cache requests that explicitly forbid caching via Cache-Control
// or Fetch API cache mode.
hasUncacheableCacheControl(req.headers) ||
isNonCacheableRequest(req.cache) ||
globalOptions.filter?.(req) === false
) {
return next(req);
Expand Down Expand Up @@ -226,8 +231,9 @@ export function transferCacheInterceptorFn(
// Request not found in cache. Make the request and cache it if on the server.
return event$.pipe(
tap((event: HttpEvent<unknown>) => {
// Only cache successful HTTP responses.
if (event instanceof HttpResponse) {
// Only cache successful HTTP responses that do not have Cache-Control
// directives that forbid shared caching (no-store or private).
if (event instanceof HttpResponse && !hasUncacheableCacheControl(event.headers)) {
transferState.set<TransferHttpResponse>(storeKey, {
[BODY]: event.body,
[HEADERS]: getFilteredHeaders(event.headers, headersToInclude),
Expand All @@ -253,6 +259,30 @@ function hasAuthHeaders(req: HttpRequest<unknown>): boolean {
);
}

const UNCACHEABLE_CACHE_CONTROL_DIRECTIVES = new Set(['no-store', 'private', 'no-cache']);

function hasUncacheableCacheControl(headers: HttpHeaders): boolean {
const cacheControl = headers.get('cache-control');

if (!cacheControl) {
return false;
}

return cacheControl.split(',').some((directive) => {
const directiveName = directive.split('=', 1)[0].trim().toLowerCase();

return UNCACHEABLE_CACHE_CONTROL_DIRECTIVES.has(directiveName);
});
}

function isNonCacheableRequest(cache: RequestCache): boolean {
return cache === 'no-cache' || cache === 'no-store';
}

function hasOutgoingCredentials(req: HttpRequest<unknown>): boolean {
return req.withCredentials || req.credentials === 'include' || req.credentials === 'same-origin';
}

function getFilteredHeaders(
headers: HttpHeaders,
includeHeaders: string[] | undefined,
Expand Down
Loading
Loading