From e2ef1ce72ae084e01a76950c731052f4fa97fcdd Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:22:37 -0500 Subject: [PATCH] fix(http): skip transfer cache for fetch credentialed requests Treat HttpClient requests using `credentials: 'include'` and `same-origin` as credentialed when deciding whether a response can be stored in the HTTP transfer cache. The transfer cache already skips requests with `withCredentials`, `Cookie`, `Authorization`, or `Proxy-Authorization` because those responses may contain user-specific data. Fetch-backed requests can express the same credentialed behavior through the `credentials` option, so these responses must not be serialized into the SSR HTML. This keeps credentialed SSR responses out of TransferState and aligns the cache eligibility check with the fetch request options supported by HttpClient. (cherry picked from commit 8ec01970d26a154fa14258a172535f2b4a0851ed) --- adev/src/content/guide/ssr.md | 4 +- packages/common/http/src/transfer_cache.ts | 9 +++- .../common/http/test/transfer_cache_spec.ts | 41 +++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/adev/src/content/guide/ssr.md b/adev/src/content/guide/ssr.md index de2ccb671ca..1bfffb5499a 100644 --- a/adev/src/content/guide/ssr.md +++ b/adev/src/content/guide/ssr.md @@ -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. You can override those settings by using `withHttpTransferCacheOptions` to the hydration configuration. ```ts import { bootstrapApplication } from '@angular/platform-browser'; @@ -417,7 +417,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({ diff --git a/packages/common/http/src/transfer_cache.ts b/packages/common/http/src/transfer_cache.ts index 689a4e2ea02..98f1a834b4f 100644 --- a/packages/common/http/src/transfer_cache.ts +++ b/packages/common/http/src/transfer_cache.ts @@ -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) * @@ -136,7 +137,7 @@ 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)) || @@ -253,6 +254,10 @@ function hasAuthHeaders(req: HttpRequest): boolean { ); } +function hasOutgoingCredentials(req: HttpRequest): boolean { + return req.withCredentials || req.credentials === 'include' || req.credentials === 'same-origin'; +} + function getFilteredHeaders( headers: HttpHeaders, includeHeaders: string[] | undefined, diff --git a/packages/common/http/test/transfer_cache_spec.ts b/packages/common/http/test/transfer_cache_spec.ts index 0d359087c6c..64537e715be 100644 --- a/packages/common/http/test/transfer_cache_spec.ts +++ b/packages/common/http/test/transfer_cache_spec.ts @@ -41,6 +41,7 @@ interface RequestParams { transferCache?: {includeHeaders: string[]} | boolean; headers?: {[key: string]: string}; withCredentials?: boolean; + credentials?: RequestCredentials; body?: RequestBody; } @@ -408,6 +409,36 @@ describe('TransferCache', () => { }); }); + it('should not cache requests with included credentials', async () => { + makeRequestAndExpectOne('/test-auth', 'foo', { + credentials: 'include', + }); + + makeRequestAndExpectOne('/test-auth', 'foo', { + credentials: 'include', + }); + }); + + it('should not cache requests with same-origin credentials', async () => { + makeRequestAndExpectOne('/test-auth', 'foo', { + credentials: 'same-origin', + }); + + makeRequestAndExpectOne('/test-auth', 'foo', { + credentials: 'same-origin', + }); + }); + + it('should cache requests with omitted credentials', async () => { + makeRequestAndExpectOne('/test-auth', 'foo', { + credentials: 'omit', + }); + + makeRequestAndExpectNone('/test-auth', 'GET', { + credentials: 'omit', + }); + }); + it('should cache POST with the differing body in string form', () => { makeRequestAndExpectOne('/test-1', null, {method: 'POST', transferCache: true, body: 'foo'}); makeRequestAndExpectNone('/test-1', 'POST', {transferCache: true, body: 'foo'}); @@ -570,6 +601,16 @@ describe('TransferCache', () => { }); }); + it(`should not cache requests with included credentials when 'includeRequestsWithAuthHeaders' is 'true'`, async () => { + makeRequestAndExpectOne('/test-auth', 'foo', { + credentials: 'include', + }); + + makeRequestAndExpectOne('/test-auth', 'foo', { + credentials: 'include', + }); + }); + it('should cache a POST request', () => { makeRequestAndExpectOne('/include?foo=1', 'post-body', {method: 'POST'});