Skip to content

fix: isolate header-bearing iOS images from URL-only HTTP caching - #963

Merged
hryhoriiK97 merged 5 commits into
software-mansion:mainfrom
pingdotgg:fix/ios-header-image-cache
Oct 10, 2026
Merged

hryhoriiK97 merged 5 commits into
software-mansion:mainfrom
pingdotgg:fix/ios-header-image-cache

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What/Why?

The decoded image cache includes request headers, but NSURLSession can return HTTP-cached bytes for the same URL under another header set. Use a session without URLCache for header-bearing requests, retaining the decoded header-aware cache and in-flight deduplication. Requests without headers keep ordinary HTTP caching.

Normalize header-name casing once for both cache identity and the actual request. Duplicate case aliases therefore resolve to the same deterministic value that is sent on the wire.

Fixes #956. This is the narrow cache fix invited in #852, separate from the deferred media API proposals.

Testing

  • Four registered XCTest cases executed successfully, with zero failures or skips. They cover case-equivalent identity, value/URL separation without plaintext secrets in keys, a seeded ordinary HTTP cache that the header session cannot consult, and an actual NSURLProtocol download.
  • The download case verifies the canonical header value on the request and that an equivalent second request reuses the decoded cache without another download.
  • Library types, Bob/module/codegen, formatting and normal hooks passed. This changes request/cache behavior, so screenshots do not apply.

Local native proof uses an isolated upstream example checkout on an iPhone 17 Pro simulator running iOS 27. Xcode used two jobs, disabled signing/index/debug symbols, and task-owned XCFrameworks retained the arm64 simulator slice to fit the host. These are host resource settings, not workflow or production changes. No Android device, VoiceOver traversal, or full Maestro-suite pass is claimed.

Base: b80be973. Current head: 156451e6. Current-head library/lint/Android checks passed. iOS checks remain queued; CI is incomplete.

PR Checklist

  • Code compiles and runs on iOS, including four executed native tests
  • Code compiles and runs on Android, unchanged and no device run
  • Updated documentation/README if applicable, no public API change
  • Ran example app interactively to verify changes, native request tests used instead
  • E2E tests are passing, full suite not run
  • Required regression tests have been added, request-level XCTest applies here

Model: GPT-6.1. Harness: Codex in T3 Code.

@juliusmarminge
juliusmarminge marked this pull request as ready for review October 8, 2026 18:49
…he on Android too

- Android (React Native and standalone packages): header-bearing requests use an
  OkHttp client without the disk cache, mirroring the iOS session split; header
  names are lower-cased once for the cache key and the request
- iOS: keep local images keyed by URL as before, drop the redundant per-request
  cache policy, document the case-insensitive key
- docs: the disk cache is bypassed for custom-header requests on both platforms
- drop the XCTest suite and its project entries

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@hryhoriiK97

hryhoriiK97 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

@juliusmarminge thanks, this is the fix Ernest asked for in #852 and the iOS half is right. I pushed a follow-up commit (7b5d83b) on top:

  • Android had the same bug. The caching guide names OkHttp alongside NSURLCache, and the Android downloader is the same design with a URL-keyed disk cache. Header-bearing requests now use a client without the cache, built from the shared one so the connection pool is reused, and header names are lower-cased for the key the same way. Applied to both the React Native package and the standalone enriched-markdown-android package, which carries its own copy of the downloader.
  • Local images stay keyed by URL. The PR dropped the isLocal branch, so a file:// image would have been re-keyed by headers. Android keys local images by URL and the docs say local sources skip request headers, so that's restored.
  • Docs updated. The caution in the image-caching guide and the docs-md mirror now describe the new behaviour: custom-header requests bypass the disk cache on both platforms and are refetched after a restart or memory eviction; header-free requests keep it.
  • Small cleanups. Dropped the per-request cache policy that duplicated the header session's configuration, and documented the case-insensitive key on the public functions.
  • Tests removed, consistent with the other PRs. The XCTest suite also reached into private ivars by KVC and swapped sessions underneath the downloader, so it would have broken on any refactor.

Verified on an iPhone 17 simulator and a Pixel 9 emulator against a local server that returns a different image per Authorization value and logs every hit: changing the header refetches and the image follows it, a case-aliased header is served from memory, remounts make no requests, and after a relaunch only the header-bearing image is fetched again while the header-free control is still served from disk.

Worth a release-notes line: images fetched with custom headers no longer persist across app launches.

cc. @jnowakow

…he in the Swift package

Same split as the other implementations: a second URLSession without a
URLCache for requests with custom headers, header names lower-cased once for
the cache key and the request. Digest pins in ImageCacheKeyTests follow the
lower-cased names. Docs state that header-bearing responses are not persisted
on disk. Comments trimmed to one line across all four copies.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@hryhoriiK97

hryhoriiK97 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

One more commit (abe57e7): the Swift package enriched-markdown-ios had the same bug, with its own URLSession and URL-keyed URLCache, and its guide promised that one user's authorized image is never served to another's session. Same fix there: a cache-less session for header-bearing requests, lower-cased header names in the key, digest pins in ImageCacheKeyTests updated, docs adjusted. Verified through the native example app with .markdownImageRequestHeaders against the same local server, same results as the other platforms. Also trimmed the comments added earlier to one line each.

That covers all four implementations: React Native iOS and Android, the standalone Android package and the Swift package.

cc. @maksg

@hryhoriiK97
hryhoriiK97 merged commit b3428c7 into software-mansion:main Oct 10, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep header-bearing image requests separate in the iOS HTTP cache

2 participants