From b0fb38efdab01f9d1aefb95251a4bf47d10a7220 Mon Sep 17 00:00:00 2001 From: Hexix23 Date: Tue, 5 May 2026 11:11:34 +0200 Subject: [PATCH] fix(http): distinguish repeated transfer cache params Serialize transfer cache request parameters without comma-joining repeated values so distinct HttpClient requests cannot reuse the same cached response. --- packages/common/http/src/transfer_cache.ts | 10 ++--- .../common/http/test/transfer_cache_spec.ts | 41 ++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/common/http/src/transfer_cache.ts b/packages/common/http/src/transfer_cache.ts index d3864aa2b467..ec1d794162ea 100644 --- a/packages/common/http/src/transfer_cache.ts +++ b/packages/common/http/src/transfer_cache.ts @@ -368,17 +368,17 @@ function getFilteredHeaders( } function sortAndConcatParams(params: HttpParams | URLSearchParams): string { - return [...params.keys()] - .sort() - .map((k) => `${k}=${params.getAll(k)}`) - .join('&'); + const searchParams = new URLSearchParams( + params instanceof URLSearchParams ? params : params.toString(), + ); + searchParams.sort(); + return searchParams.toString(); } function makeCacheKey( request: HttpRequest, mappedRequestUrl: string, ): StateKey { - // make the params encoded same as a url so it's easy to identify const {params, method, responseType} = request; const encodedParams = sortAndConcatParams(params); diff --git a/packages/common/http/test/transfer_cache_spec.ts b/packages/common/http/test/transfer_cache_spec.ts index 79cd88413ca6..f957996cf862 100644 --- a/packages/common/http/test/transfer_cache_spec.ts +++ b/packages/common/http/test/transfer_cache_spec.ts @@ -19,7 +19,14 @@ import {TestBed} from '@angular/core/testing'; import {useAutoTick, timeout, withBody} from '@angular/private/testing'; import {BehaviorSubject, Observable, of} from 'rxjs'; -import {HttpClient, HttpHeaders, HttpRequest, HttpResponse, provideHttpClient} from '../public_api'; +import { + HttpClient, + HttpHeaders, + HttpParams, + HttpRequest, + HttpResponse, + provideHttpClient, +} from '../public_api'; import { BODY, CACHE_OPTIONS, @@ -432,6 +439,38 @@ describe('TransferCache', () => { ); }); + it('should differentiate repeated parameters from scalar comma parameters', () => { + let scalarResponse!: string; + TestBed.inject(HttpClient) + .get('/test-params', {params: new HttpParams().set('role', 'user,admin')}) + .subscribe((response) => (scalarResponse = response as string)); + TestBed.inject(HttpTestingController) + .expectOne('/test-params?role=user,admin') + .flush('scalar'); + + let repeatedResponse!: string; + TestBed.inject(HttpClient) + .get('/test-params', { + params: new HttpParams().append('role', 'user').append('role', 'admin'), + }) + .subscribe((response) => (repeatedResponse = response as string)); + TestBed.inject(HttpTestingController) + .expectOne('/test-params?role=user&role=admin') + .flush('repeated'); + + let repeatedCachedResponse!: string; + TestBed.inject(HttpClient) + .get('/test-params', { + params: new HttpParams().append('role', 'user').append('role', 'admin'), + }) + .subscribe((response) => (repeatedCachedResponse = response as string)); + TestBed.inject(HttpTestingController).expectNone('/test-params?role=user&role=admin'); + + expect(scalarResponse).toBe('scalar'); + expect(repeatedResponse).toBe('repeated'); + expect(repeatedCachedResponse).toBe('repeated'); + }); + it('should skip cache when specified', () => { makeRequestAndExpectOne('/test-1?foo=1', 'foo', {transferCache: false}); // The previous request wasn't cached so this one can't use the cache