feat(http): add options to allow caching of credentialed and non-cacheable HTTP requests

Adds `includeRequestsWithCredentials` and `includeNonCacheableRequests` options to `HttpTransferCacheOptions`.
This commit is contained in:
Alan Agius
2026-07-06 23:03:32 +02:00
committed by GitHub
parent 8e090cbe82
commit e3630c23c5
6 changed files with 179 additions and 43 deletions
+1 -3
View File
@@ -90,9 +90,7 @@ The Event Replay is divided into three main phases:
Event replay supports _native browser events_, for example `click`, `mouseover`, and `focusin`. If you'd like to learn more about JSAction, the library that powers event replay, you can read more [on the readme](https://github.com/angular/angular/tree/main/packages/core/primitives/event-dispatch#readme).
---
This feature ensures a consistent user experience, preventing user actions performed before Hydration from being ignored.
This feature ensures a consistent user experience, preventing user actions performed before hydration from being ignored.
NOTE: If you have [incremental hydration](guide/incremental-hydration) enabled, event replay is automatically enabled under the hood.
+27 -7
View File
@@ -478,8 +478,6 @@ bootstrapApplication(App, {
});
```
---
### `includeHeaders`
Specifies which headers from the server response should be included in cached entries.
@@ -495,8 +493,6 @@ IMPORTANT: Avoid including sensitive headers like authentication tokens. These c
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`
By default, only `GET` and `HEAD` requests are cached.
@@ -510,12 +506,10 @@ withHttpTransferCacheOptions({
Use this only when `POST` requests are **idempotent** and safe to reuse between server and client renders.
---
### `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` or Fetch API `credentials` set to `include` or `same-origin` are also excluded by default.
By default, these are excluded to prevent caching user‑specific responses.
```ts
withHttpTransferCacheOptions({
@@ -525,6 +519,32 @@ withHttpTransferCacheOptions({
Enable only when authentication headers do **not** affect the response content (for example, public tokens for analytics APIs).
### `includeRequestsWithCredentials`
Determines whether requests sent using `withCredentials` or Fetch API `credentials` modes (`include` or `same-origin`) are eligible for caching.
By default, these are excluded to prevent caching user‑specific responses.
```ts
withHttpTransferCacheOptions({
includeRequestsWithCredentials: true,
});
```
Enable only when credentialed requests return responses that are safe to cache.
### `includeNonCacheableRequests`
Determines whether requests and responses containing `Cache-Control` directives that forbid caching (`no-store`, `no-cache`, or `private`), responses with a `Set-Cookie` header, or requests using Fetch API `cache` options (`no-store` or `no-cache`), are eligible for caching.
By default, these are excluded to respect HTTP caching controls.
```ts
withHttpTransferCacheOptions({
includeNonCacheableRequests: true,
});
```
Enable only when you need to bypass cache-control restrictions for transfer caching.
### Per‑request overrides
You can override caching behavior for a specific request using the `transferCache` request option.
+5 -3
View File
@@ -1164,12 +1164,14 @@ export enum HttpStatusCode {
}
// @public
export type HttpTransferCacheOptions = {
includeHeaders?: string[];
export interface HttpTransferCacheOptions {
filter?: (req: HttpRequest<unknown>) => boolean;
includeHeaders?: string[];
includeNonCacheableRequests?: boolean;
includePostRequests?: boolean;
includeRequestsWithAuthHeaders?: boolean;
};
includeRequestsWithCredentials?: boolean;
}
// @public
export interface HttpUploadProgressEvent extends HttpProgressEvent {
+55 -29
View File
@@ -33,28 +33,47 @@ import {HttpParams} from './params';
/**
* Options to configure how TransferCache should be used to cache requests made via HttpClient.
*
* @param includeHeaders Specifies which headers should be included into cached responses. No
* headers are included by default.
* @param filter A function that receives a request as an argument and returns a boolean to indicate
* whether a request should be included into the cache.
* @param includePostRequests Enables caching for POST requests. By default, only GET and HEAD
* requests are cached. This option can be enabled if POST requests are used to retrieve data
* (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` 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)
*
* @publicApi
*/
export type HttpTransferCacheOptions = {
includeHeaders?: string[];
export interface HttpTransferCacheOptions {
/**
* A function that receives a request as an argument and returns a boolean to indicate
* whether a request should be included into the cache.
*/
filter?: (req: HttpRequest<unknown>) => boolean;
/**
* Specifies which headers should be included into cached responses. No headers are included by default.
*/
includeHeaders?: string[];
/**
* Enables caching for `POST` requests. By default, only `GET` and `HEAD` requests are cached.
* This option can be enabled if `POST` requests are used to retrieve data (for example using `GraphQL`).
*/
includePostRequests?: boolean;
/**
* Enables caching of requests containing `Authorization`, `Proxy-Authorization`, or `Cookie` headers.
* By default, these requests are excluded from caching.
*/
includeRequestsWithAuthHeaders?: boolean;
};
/**
* Enables caching of requests sent using `withCredentials` or Fetch API `credentials` modes (`include` or `same-origin`).
* By default, these requests are excluded from caching.
*/
includeRequestsWithCredentials?: boolean;
/**
* Enables caching of requests and responses with `Cache-Control` directives that forbid caching
* (such as `no-cache`, `no-store`, or `private`), responses with a `Set-Cookie` header, or requests using Fetch API `no-cache` or `no-store` modes.
* By default, these requests/responses are excluded from caching.
*/
includeNonCacheableRequests?: boolean;
}
/**
* If your application uses different HTTP origins to make API calls (via `HttpClient`) on the server and
@@ -126,24 +145,31 @@ export const CACHE_OPTIONS = new InjectionToken<CacheOptions>(
const ALLOWED_METHODS = ['GET', 'HEAD'];
function canUseOrCacheRequest(req: HttpRequest<unknown>, options: CacheOptions): boolean {
const {isCacheActive, ...globalOptions} = options;
const {
isCacheActive,
filter,
includePostRequests,
includeRequestsWithAuthHeaders,
includeRequestsWithCredentials,
includeNonCacheableRequests,
} = options;
const {transferCache: requestOptions, method: requestMethod} = req;
if (
!isCacheActive ||
requestOptions === false ||
// Do not cache requests sent with credentials.
hasOutgoingCredentials(req) ||
// POST requests are allowed either globally or at request level
(requestMethod === 'POST' && !globalOptions.includePostRequests && !requestOptions) ||
(requestMethod === 'POST' && !includePostRequests && !requestOptions) ||
(requestMethod !== 'POST' && !ALLOWED_METHODS.includes(requestMethod)) ||
// Do not cache requests with authentication or cookie headers unless explicitly enabled.
(!globalOptions.includeRequestsWithAuthHeaders && hasAuthHeaders(req)) ||
(!includeRequestsWithAuthHeaders && hasAuthHeaders(req)) ||
// Do not cache requests sent with credentials unless explicitly enabled.
(!includeRequestsWithCredentials && hasOutgoingCredentials(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
// or Fetch API cache mode unless explicitly enabled.
(!includeNonCacheableRequests &&
(hasUncacheableCacheControl(req.headers) || isNonCacheableRequest(req.cache))) ||
filter?.(req) === false
) {
return false;
}
@@ -287,11 +313,11 @@ export function transferCacheInterceptorFn(
if (event instanceof HttpResponse) {
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) and do not
// carry a Set-Cookie header. A Set-Cookie header marks the response as
// user-specific.
if (hasUncacheableCacheControl(headers) || hasSetCookieHeader(headers)) {
// Only cache successful HTTP responses that are not non-cacheable.
if (
!options.includeNonCacheableRequests &&
(hasUncacheableCacheControl(headers) || hasSetCookieHeader(headers))
) {
return;
}
@@ -960,6 +960,97 @@ describe('TransferCache', () => {
});
});
describe('caching with includeRequestsWithCredentials and includeNonCacheableRequests', () => {
beforeEach(
withBody('<test-app-http></test-app-http>', () => {
TestBed.resetTestingModule();
isStable = new BehaviorSubject<boolean>(false);
@Injectable()
class ApplicationRefPatched extends ApplicationRef {
override get isStable() {
return new BehaviorSubject<boolean>(false);
}
}
TestBed.configureTestingModule({
declarations: [SomeComponent],
providers: [
{provide: PLATFORM_ID, useValue: PLATFORM_SERVER_ID},
{provide: DOCUMENT, useFactory: () => document},
{provide: ApplicationRef, useClass: ApplicationRefPatched},
withHttpTransferCache({
includeRequestsWithCredentials: true,
includeNonCacheableRequests: true,
}),
provideHttpClient(),
provideHttpClientTesting(),
],
});
const appRef = TestBed.inject(ApplicationRef);
appRef.bootstrap(SomeComponent);
isStable = appRef.isStable as BehaviorSubject<boolean>;
}),
);
it(`should cache requests with credentials when 'includeRequestsWithCredentials' is 'true'`, async () => {
makeRequestAndExpectOne('/test-cred', 'foo', {
withCredentials: true,
});
makeRequestAndExpectNone('/test-cred');
});
it(`should cache requests with included credentials mode when 'includeRequestsWithCredentials' is 'true'`, async () => {
makeRequestAndExpectOne('/test-cred', 'foo', {
credentials: 'include',
});
makeRequestAndExpectNone('/test-cred');
});
it(`should cache requests with same-origin credentials mode when 'includeRequestsWithCredentials' is 'true'`, async () => {
makeRequestAndExpectOne('/test-cred', 'foo', {
credentials: 'same-origin',
});
makeRequestAndExpectNone('/test-cred');
});
it(`should cache responses with Cache-Control: no-store when 'includeNonCacheableRequests' is 'true'`, async () => {
makeRequestAndExpectOne('/test-cache-control', 'foo', {
responseHeaders: {'Cache-Control': 'no-store'},
});
makeRequestAndExpectNone('/test-cache-control');
});
it(`should cache responses with Cache-Control: private when 'includeNonCacheableRequests' is 'true'`, async () => {
makeRequestAndExpectOne('/test-cache-control', 'foo', {
responseHeaders: {'Cache-Control': 'private'},
});
makeRequestAndExpectNone('/test-cache-control');
});
it(`should cache requests with Cache-Control: no-cache when 'includeNonCacheableRequests' is 'true'`, async () => {
makeRequestAndExpectOne('/test-cache-control', 'foo', {
headers: {'Cache-Control': 'no-cache'},
});
makeRequestAndExpectNone('/test-cache-control');
});
it(`should cache requests with cache: 'no-store' mode when 'includeNonCacheableRequests' is 'true'`, async () => {
makeRequestAndExpectOne('/test-cache-mode', 'foo', {
cache: 'no-store',
});
makeRequestAndExpectNone('/test-cache-mode');
});
});
describe('caching with public origins', () => {
beforeEach(
withBody('<test-app-http></test-app-http>', () => {
@@ -311,7 +311,6 @@
"__getOwnPropDescs",
"__getOwnPropSymbols",
"__hasOwnProp",
"__objRest",
"__propIsEnum",
"__spreadProps",
"__spreadValues",