Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fixup! fix(http): correctly cache blob responses in transfer cache
Previously, Blob values were passed to `Uint8Array` this resulted in silently producing an empty array (length = 0) without throwing an error, leading to empty cached data
  • Loading branch information
SkyZeroZx committed Mar 4, 2026
commit 6a0a9ed0b57c734f222f1d99081fb7675ddd02b6
49 changes: 20 additions & 29 deletions packages/common/http/src/transfer_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
ɵtruncateMiddle as truncateMiddle,
ɵRuntimeError as RuntimeError,
} from '@angular/core';
import {Observable, of, from} from 'rxjs';
import {tap, concatMap} from 'rxjs/operators';
import {Observable, of} from 'rxjs';
import {concatMap} from 'rxjs/operators';

import {RuntimeErrorCode} from './errors';
import {HttpHeaders} from './headers';
Expand Down Expand Up @@ -266,41 +266,32 @@ export function transferCacheInterceptorFn(
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
// Request not found in cache. Make the request and cache it if on the server.
return event$.pipe(
concatMap((event: HttpEvent<unknown>) => {
concatMap(async (event: HttpEvent<unknown>) => {
// Only cache successful HTTP responses.
if (event instanceof HttpResponse) {
const storeInCache = (body: unknown) => {
transferState.set<TransferHttpResponse>(storeKey, {
[BODY]: body,
[HEADERS]: getFilteredHeaders(event.headers, headersToInclude),
[STATUS]: event.status,
[STATUS_TEXT]: event.statusText,
[REQ_URL]: requestUrl,
[RESPONSE_TYPE]: req.responseType,
});
};

let body = event.body;
if (req.responseType === 'blob') {
// Convert Blob to ArrayBuffer asynchronously before caching.
// Note: Blob is converted to ArrayBuffer because Uint8Array constructor
// doesn't accept Blob directly, which would result in an empty array.
// Type assertion is safe here: when responseType is 'blob', the body is guaranteed to be a Blob
return from((event.body as Blob).arrayBuffer()).pipe(
tap((arrayBuffer) => storeInCache(toBase64(arrayBuffer))),
concatMap(() => of(event)),
);
const arrayBuffer = await (event.body as Blob).arrayBuffer();
body = toBase64(arrayBuffer);
} else if (req.responseType === 'arraybuffer') {
// For arraybuffer, convert to base64; for other types (json, text), store as-is.
// Type assertion is safe here: when responseType is 'arraybuffer', the body is
// guaranteed to be an ArrayBuffer
body = toBase64(event.body as ArrayBufferLike);
}

// For arraybuffer, convert to base64; for other types (json, text), store as-is.
// Type assertion is safe here: when responseType is 'arraybuffer', the body is
// guaranteed to be an ArrayBuffer
const body =
req.responseType === 'arraybuffer'
? toBase64(event.body as ArrayBufferLike)
: event.body;
storeInCache(body);
transferState.set<TransferHttpResponse>(storeKey, {
[BODY]: body,
[HEADERS]: getFilteredHeaders(event.headers, headersToInclude),
[STATUS]: event.status,
[STATUS_TEXT]: event.statusText,
[REQ_URL]: requestUrl,
[RESPONSE_TYPE]: req.responseType,
});
}
return of(event);
return event;
}),
);
}
Expand Down
Loading