Skip to content

HttpTransferCache stores an empty 0-byte Blob when caching responseType: 'blob' during SSR #71917

Open

Description

@halcyon-past

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

  1. halcyon-past commented on Nov 10, 2026

    @halcyon-past
    Author

    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 Blob responses back from TransferState.

    Since Blob.prototype.arrayBuffer() is asynchronous while transferCacheInterceptorFn currently caches synchronously inside tap(), I'd also appreciate any preference on the approach (e.g., converting Blob bodies asynchronously via concatMap during ngServerMode and optionally preserving blob.type in TransferHttpResponse) before I open a PR.

  2. added this to the needsTriage milestone on Nov 10, 2026
  3. SkyZeroZx commented on Nov 10, 2026

    @SkyZeroZx
    Contributor

    Hi, this is a duplicate of #70226

    we currently have an open PR: #70227

  4. halcyon-past commented on Nov 10, 2026

    @halcyon-past
    Author

    Thanks for taking a look @SkyZeroZx and @JeanMeche — however, #70226 / #70227 only addresses outgoing POST request bodies in makeCacheKey() (preventing cache-key collisions when sending Blob / FormData / ArrayBuffer payloads).

    This issue (#71917) is about cached response bodies when responseType === 'blob' (for example, a GET request via HttpClient.get(url, {responseType: 'blob'}) or httpResource.blob()). Since GET requests have serializedBody === null, PR #70227 still caches them, and transferCacheInterceptorFn still passes the HttpResponse's Blob body to toBase64(), which runs new Uint8Array(blob) and stores "" in TransferState.

    Could we reopen #71917 since response-side Blob caching isn't covered by #70227?

  5. SkyZeroZx commented on Nov 10, 2026

    @SkyZeroZx
    Contributor

    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.

  6. halcyon-past commented on Nov 10, 2026

    @halcyon-past
    Author

    Good point to bring up, @SkyZeroZx — just to clarify, the limitation in PR #70227 only applies to outgoing POST requests with Blob/FormData request bodies (as noted under includePostRequests in ssr.md). For a GET request with responseType: 'blob' (or httpResource.blob()), there is no Blob in the request—the cache key is derived synchronously from the URL, method, and params as usual. #71917 is strictly about serializing the response Blob in transferCacheInterceptorFn / toBase64().

    Because #70227 only touches request-side cache keys in makeCacheKey() and skips Blob/FormData request payloads, fixing response-side Blob caching 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 Blob bytes and type via body.arrayBuffer() during ngServerMode) and would like to open the PR for it once you're happy with the approach.

  7. SkyZeroZx commented on Nov 10, 2026

    @SkyZeroZx
    Contributor

    You are right , my bad.

    preserving Blob bytes and type via body.arrayBuffer() during ngServerMode

    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 the httpResource one from #67307. So it couldn't land that way, and blob responses stayed unfixed.

  8. halcyon-past commented on Nov 10, 2026

    @halcyon-past
    Author

    Thanks for the context on #67002 / #67439, @SkyZeroZx — that's super helpful to know!

    Looking at #67002, it passed an async callback directly to concatMap(async (event) => ...), which turned every response (json, text, arraybuffer, etc.) into a Promise and broke synchronous httpResource / hydration_spec / transfer_cache_spec tests.

    In my local fix, I avoided that by keeping the synchronous path via of(event) for all non-Blob responses and only returning from(body.arrayBuffer()) when responseType === 'blob' && body instanceof Blob. With that change, all existing httpResource, transfer_cache_spec, hydration_spec, and platform-server tests continue to run synchronously and pass unmodified.

    Happy to wait for @JeanMeche / the team's go-ahead and assignment before opening the PR!

  9. atscott commented on Nov 10, 2026

    @atscott
    Contributor

    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.

  10. added a commit that references this issue on Nov 11, 2026
    4c60e4d
  11. SkyZeroZx commented on Nov 11, 2026

    @SkyZeroZx
    Contributor

    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.

    I just added a new commit to #70227 that skips responseType: 'blob' in HttpTransferCache, 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

  12. added a commit that references this issue on Nov 11, 2026
    0fbaa4c
  13. halcyon-past commented on Nov 12, 2026

    @halcyon-past
    Author

    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 SSR Blob caching sounds good to me. Happy to pick it back up if the team decides to support it later!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: common/httpIssues related to HTTP and HTTP Client

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions