Skip to content

Commit 3905015

Browse files
SkyZeroZxAndrewKushnir
authored andcommitted
fix(http): correctly parse ArrayBuffer and Blob in transfer cache
Encodes arraybuffer and blob response bodies as base64 when storing in the transfer cache, ensuring correct retrieval and usage on the client side. Fixes #66827 (cherry picked from commit cb1163e)
1 parent 6f5c233 commit 3905015

3 files changed

Lines changed: 60 additions & 3 deletions

File tree

packages/common/http/src/transfer_cache.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,10 @@ export function transferCacheInterceptorFn(
188188

189189
switch (responseType) {
190190
case 'arraybuffer':
191-
body = new TextEncoder().encode(undecodedBody).buffer;
191+
body = fromBase64(undecodedBody);
192192
break;
193193
case 'blob':
194-
body = new Blob([undecodedBody]);
194+
body = new Blob([fromBase64(undecodedBody)]);
195195
break;
196196
}
197197

@@ -226,7 +226,10 @@ export function transferCacheInterceptorFn(
226226
// Only cache successful HTTP responses.
227227
if (event instanceof HttpResponse) {
228228
transferState.set<TransferHttpResponse>(storeKey, {
229-
[BODY]: event.body,
229+
[BODY]:
230+
req.responseType === 'arraybuffer' || req.responseType === 'blob'
231+
? toBase64(event.body)
232+
: event.body,
230233
[HEADERS]: getFilteredHeaders(event.headers, headersToInclude),
231234
[STATUS]: event.status,
232235
[STATUS_TEXT]: event.statusText,
@@ -313,6 +316,28 @@ function generateHash(value: string): string {
313316
return hash.toString();
314317
}
315318

319+
function toBase64(buffer: unknown): string {
320+
//TODO: replace with when is Baseline widely available
321+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64
322+
const bytes = new Uint8Array(buffer as ArrayBufferLike);
323+
324+
const CHUNK_SIZE = 0x8000; // 32,768 bytes (~32 KB) per chunk, to avoid stack overflow
325+
326+
let binaryString = '';
327+
328+
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
329+
const chunk = bytes.subarray(i, i + CHUNK_SIZE);
330+
binaryString += String.fromCharCode.apply(null, chunk as unknown as number[]);
331+
}
332+
return btoa(binaryString);
333+
}
334+
335+
function fromBase64(base64: string): ArrayBuffer {
336+
const binary = atob(base64);
337+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
338+
return bytes.buffer;
339+
}
340+
316341
/**
317342
* Returns the DI providers needed to enable HTTP transfer cache.
318343
*

packages/common/http/test/transfer_cache_spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,37 @@ describe('TransferCache', () => {
138138
expect(transferState.get(key, null)).toEqual(jasmine.objectContaining({[BODY]: 'foo'}));
139139
});
140140

141+
it('should cache arraybuffer responses correctly', () => {
142+
const testData = new Uint8Array([1, 2, 3, 4, 5]).buffer;
143+
let response!: ArrayBuffer;
144+
TestBed.inject(HttpClient)
145+
.get('/test-arraybuffer', {responseType: 'arraybuffer'})
146+
.subscribe((r) => (response = r));
147+
TestBed.inject(HttpTestingController).expectOne('/test-arraybuffer').flush(testData);
148+
149+
expect(new Uint8Array(response)).toEqual(new Uint8Array([1, 2, 3, 4, 5]));
150+
151+
let cachedResponse!: ArrayBuffer;
152+
TestBed.inject(HttpClient)
153+
.get('/test-arraybuffer', {responseType: 'arraybuffer'})
154+
.subscribe((r) => (cachedResponse = r));
155+
TestBed.inject(HttpTestingController).expectNone('/test-arraybuffer');
156+
157+
expect(new Uint8Array(cachedResponse)).toEqual(new Uint8Array([1, 2, 3, 4, 5]));
158+
});
159+
160+
it('should cache blob responses correctly', () => {
161+
const testData = new Uint8Array([10, 20, 30, 40, 50]).buffer;
162+
let response!: Blob;
163+
TestBed.inject(HttpClient)
164+
.get('/test-blob', {responseType: 'blob'})
165+
.subscribe((r) => (response = r));
166+
TestBed.inject(HttpTestingController).expectOne('/test-blob').flush(testData);
167+
168+
expect(response instanceof Blob).toBeTrue();
169+
expect(response.size).toBe(5);
170+
});
171+
141172
it('should stop storing HTTP calls in `TransferState` after application becomes stable', fakeAsync(() => {
142173
makeRequestAndExpectOne('/test-1', 'foo');
143174
makeRequestAndExpectOne('/test-2', 'buzz');

packages/core/test/bundling/hydration/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@
445445
"from",
446446
"fromArrayLike",
447447
"fromAsyncIterable",
448+
"fromBase64",
448449
"fromInteropObservable",
449450
"fromIterable",
450451
"fromPromise",

0 commit comments

Comments
 (0)