Skip to content
Open
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
4 changes: 2 additions & 2 deletions adev/src/content/guide/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,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` 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.
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`. Responses that carry a `Set-Cookie` header are also skipped. 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 @@ -560,7 +560,7 @@ 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.
`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. Responses that include a `Set-Cookie` header are likewise not stored, as they typically carry user-specific state.

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.

Expand Down
10 changes: 8 additions & 2 deletions packages/common/http/src/transfer_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,10 @@ export function transferCacheInterceptorFn(
const {headers, body, status, statusText} = event;

// Only cache successful HTTP responses that do not have Cache-Control
// directives that forbid shared caching (no-store or private).
if (hasUncacheableCacheControl(headers)) {
// directives that forbid shared caching (no-store or private) and do not
// carry a Set-Cookie header. A Set-Cookie header marks the response as
// user-specific.
if (hasUncacheableCacheControl(headers) || hasSetCookieHeader(headers)) {
Comment thread
alan-agius4 marked this conversation as resolved.
return;
}

Expand Down Expand Up @@ -338,6 +340,10 @@ function hasUncacheableCacheControl(headers: HttpHeaders): boolean {
});
}

function hasSetCookieHeader(headers: HttpHeaders): boolean {
return headers.has('set-cookie');
}

function isNonCacheableRequest(cache: RequestCache): boolean {
return cache === 'no-cache' || cache === 'no-store';
}
Expand Down
34 changes: 34 additions & 0 deletions packages/common/http/test/transfer_cache_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,32 @@ describe('TransferCache', () => {
expect(secondNext).toHaveBeenCalledTimes(1);
});

it('should not cache responses with a Set-Cookie header', () => {
configureInterceptor();

const request = new HttpRequest('GET', '/test-set-cookie');

const firstNext = jasmine.createSpy('firstNext').and.returnValue(
of(
new HttpResponse({
body: 'user-a-session',
headers: new HttpHeaders({'Set-Cookie': 'session=user-a; HttpOnly'}),
}),
),
);
const secondNext = jasmine
.createSpy('secondNext')
.and.returnValue(of(new HttpResponse({body: 'user-b-session'})));

runOnServer(() => {
expect(runInterceptor(request, firstNext).body).toBe('user-a-session');
expect(runInterceptor(request, secondNext).body).toBe('user-b-session');
});

expect(firstNext).toHaveBeenCalledTimes(1);
expect(secondNext).toHaveBeenCalledTimes(1);
});

it('should not cache requests with Cache-Control: no-store', () => {
configureInterceptor();

Expand Down Expand Up @@ -630,6 +656,14 @@ describe('TransferCache', () => {
makeRequestAndExpectOne('/test-private', 'fresh-data');
});

it('should not cache responses with a Set-Cookie header', () => {
makeRequestAndExpectOne('/test-set-cookie', 'user-a-session', {
responseHeaders: {'Set-Cookie': 'session=user-a; HttpOnly'},
});

makeRequestAndExpectOne('/test-set-cookie', 'user-b-session');
});

it('should not cache responses with Cache-Control containing no-store among other directives', () => {
makeRequestAndExpectOne('/test-multi', 'data', {
responseHeaders: {'Cache-Control': 'max-age=0, no-store, must-revalidate'},
Expand Down
Loading