Repository navigation
Description
Which @angular/* package(s) are the source of the bug?
common
Is this a regression?
No
Description
When HttpTransferCache is active during SSR (enabled by default via provideClientHydration()), fetching a Blob via HttpClient.get(..., {responseType: 'blob'}) or httpResource.blob(...) serializes an empty string ("") into TransferState. During client hydration, reading that cached entry returns a 0-byte Blob with type === '' instead of the server response body.
This happens because transferCacheInterceptorFn passes Blob bodies straight to toBase64, which calls new Uint8Array(buffer as ArrayBufferLike). Because a Blob is not an ArrayBufferLike, new Uint8Array(blob) evaluates to a 0-length Uint8Array and toBase64 returns "". On the client, retrieveStateFromCache then reconstructs new Blob([fromBase64("")]).
The existing 'should cache blob responses correctly' spec in transfer_cache_spec.ts missed this because it only checks the initial HttpTestingController response and never inspects TransferState or makes a follow-up request to read from the cache (unlike the arraybuffer spec right above it).
Minimal reproduction (in packages/common/http/test/transfer_cache_spec.ts with withHttpTransferCache({})):
const testBlob = new Blob([new Uint8Array([10, 20, 30, 40, 50])], {
type: 'application/octet-stream',
});
// 1. Server request
globalThis['ngServerMode'] = true;
TestBed.inject(HttpClient).get('/test-blob', {responseType: 'blob'}).subscribe();
TestBed.inject(HttpTestingController).expectOne('/test-blob').flush(testBlob);
// TransferState now stores { b: "" }
// 2. Client hydration reads from TransferState
globalThis['ngServerMode'] = false;
let cachedResponse!: Blob;
TestBed.inject(HttpClient)
.get('/test-blob', {responseType: 'blob'})
.subscribe((res) => (cachedResponse = res));
TestBed.inject(HttpTestingController).expectNone('/test-blob');
// Actual: cachedResponse.size === 0, cachedResponse.type === ''
// Expected: cachedResponse.size === 5, cachedResponse.type === 'application/octet-stream'Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
No exception is thrown; TransferState silently stores b: "" and the client receives an empty 0-byte Blob.
Please provide the environment you discovered this bug in (run ng version)
Angular: 22.3.0-next.4 (main @ 0dafb36834)
Node.js: v26.10.0
OS: macOS arm64 (Darwin 25.5.0)
Anything else?
Related to #66827 and PR #66835, which added base64 encoding for arraybuffer and blob responses in HttpTransferCache.
Activity
halcyon-past commentedon Nov 10, 2026
Hi! Could you please assign this issue to me? I'd like to work on fixing it and adding a regression test for reading cached
Blobresponses back fromTransferState.Since
Blob.prototype.arrayBuffer()is asynchronous whiletransferCacheInterceptorFncurrently caches synchronously insidetap(), I'd also appreciate any preference on the approach (e.g., convertingBlobbodies asynchronously viaconcatMapduringngServerModeand optionally preservingblob.typeinTransferHttpResponse) before I open a PR.SkyZeroZx commentedon Nov 10, 2026
Hi, this is a duplicate of #70226
we currently have an open PR: #70227
halcyon-past commentedon Nov 10, 2026
Thanks for taking a look @SkyZeroZx and @JeanMeche — however, #70226 / #70227 only addresses outgoing
POSTrequest bodies inmakeCacheKey()(preventing cache-key collisions when sendingBlob/FormData/ArrayBufferpayloads).This issue (#71917) is about cached response bodies when
responseType === 'blob'(for example, aGETrequest viaHttpClient.get(url, {responseType: 'blob'})orhttpResource.blob()). SinceGETrequests haveserializedBody === null, PR #70227 still caches them, andtransferCacheInterceptorFnstill passes theHttpResponse'sBlobbody totoBase64(), which runsnew Uint8Array(blob)and stores""inTransferState.Could we reopen #71917 since response-side
Blobcaching isn't covered by #70227?SkyZeroZx commentedon Nov 10, 2026
Blob caching, as I understand it, will not be covered because we can´t derive a complete cache key for them synchronously, so collisions can occur without the user being aware of them.
That's why, in #70227, we are updating the documentation to explain this.
halcyon-past commentedon Nov 10, 2026
Good point to bring up, @SkyZeroZx — just to clarify, the limitation in PR #70227 only applies to outgoing
POSTrequests withBlob/FormDatarequest bodies (as noted underincludePostRequestsinssr.md). For aGETrequest withresponseType: 'blob'(orhttpResource.blob()), there is noBlobin the request—the cache key is derived synchronously from the URL, method, and params as usual. #71917 is strictly about serializing the responseBlobintransferCacheInterceptorFn/toBase64().Because #70227 only touches request-side cache keys in
makeCacheKey()and skipsBlob/FormDatarequest payloads, fixing response-sideBlobcaching here won't conflict with #70227 (and I'll make sure to rebase if #70227 lands first).@JeanMeche Could you please assign #71917 to me? I already have a working fix and regression tests ready locally (preserving
Blobbytes andtypeviabody.arrayBuffer()duringngServerMode) and would like to open the PR for it once you're happy with the approach.SkyZeroZx commentedon Nov 10, 2026
You are right , my bad.
Just for context, I tried this before in #67002, and it was reverted in #67439. To read the Blob with
arrayBuffer(), that PR made the server side of the transfer cache async, which changed the current sync behavior and broke tests like thehttpResourceone from #67307. So it couldn't land that way, and blob responses stayed unfixed.halcyon-past commentedon Nov 10, 2026
Thanks for the context on #67002 / #67439, @SkyZeroZx — that's super helpful to know!
Looking at #67002, it passed an
asynccallback directly toconcatMap(async (event) => ...), which turned every response (json,text,arraybuffer, etc.) into aPromiseand broke synchronoushttpResource/hydration_spec/transfer_cache_spectests.In my local fix, I avoided that by keeping the synchronous path via
of(event)for all non-Blobresponses and only returningfrom(body.arrayBuffer())whenresponseType === 'blob' && body instanceof Blob. With that change, all existinghttpResource,transfer_cache_spec,hydration_spec, andplatform-servertests continue to run synchronously and pass unmodified.Happy to wait for @JeanMeche / the team's go-ahead and assignment before opening the PR!
atscott commentedon Nov 10, 2026
I think the most practical path forward right now is to remove the broken blob caching (skipping responseType === 'blob' in HttpTransferCache) and associated test. That means probably just updating #70227 to skip blob as well.
As for a fix forward to actually make blob work with SSR, we should hold off until there is community feedback indicating it's something we should do.
fix(http): skip blob responses in transfer cache
SkyZeroZx commentedon Nov 11, 2026
I just added a new commit to #70227 that skips
responseType: 'blob'inHttpTransferCache, so these requests just go to the network on the client instead of hydrating an empty Blob.Also we can leave this issue open to continue to collect feedback
fix(http): skip blob responses in transfer cache
halcyon-past commentedon Nov 12, 2026
Makes complete sense — skipping
responseType: 'blob'in #70227 avoids the silent 0-byte corruption cleanly, and keeping this issue open to gauge demand for full SSRBlobcaching sounds good to me. Happy to pick it back up if the team decides to support it later!