Skip to content

fix: send source headers only to the URL's origin across redirects - #74

Merged
jkasprzyk17 merged 1 commit into
mainfrom
fix/redirect-policy
Oct 21, 2026
Merged

jkasprzyk17 merged 1 commit into
mainfrom
fix/redirect-policy

Conversation

@jkasprzyk17

@jkasprzyk17 jkasprzyk17 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Remote downloads followed redirects with each platform's defaults, which disagreed and sent credentials on to other hosts (#70). Both platforms now follow one RedirectPolicy (Kotlin and Swift):

Before: Android Before: iOS Now: both
Same origin followed, headers kept followed, Authorization dropped followed, headers kept
Other host / port / scheme followed, all headers but Authorization sent followed, all headers but Authorization sent followed, no source headers for the rest of the chain
http → https http-error 3xx followed followed (another origin: no headers)
https → http http-error 3xx followed (ATS may block) http-error with the 3xx
Limit 20 16 10, then network-error
Stale copy shown after a refused redirect / a loop no / yes (an IOException) yes / yes (a URLError) no / no: the server answered

The "Before: Android" column is AOSP's OkHttp source as read in #70, not a device run of the old build; the "Before: iOS" column was reproduced on the iOS 26.5 simulator (see Testing).

If-None-Match / If-Modified-Since go on every hop, so a 304 from the target still validates the stored copy.

  • Android follows redirects itself (instanceFollowRedirects = false), one connection per hop; cancel() disconnects the live one.
  • iOS rebuilds each redirected request in a per-task URLSessionTaskDelegate (iOS 15+): URLSession's proposed request has already lost Authorization, even on the same origin.
  • Docs: headers TSDoc, a "Redirects" section in the sources guide, the offline guide, two troubleshooting rows, the migration guide, the README guide table, docs/api.md, ARCHITECTURE.md and api-design.md.
  • Example / e2e: the fixture server gains /__redirect?to=, /__redirect-loop and ?require= / ?forbid= header checks (401 / 403). Three example entries and the new viewer-redirects flow cover a same-origin redirect with Authorization, a cross-origin one (localhost → 127.0.0.1, which adb reverse covers) and a loop.

Behavior change: an http:// URL that redirects to https:// on the same host changes origin, so headers are not sent to the https URL (as the Fetch standard does with Authorization). Send credentials to the https URL directly. No opt-in to keep headers across origins; that can wait for a request.

Closes #70

Testing

  • JVM (DocumentCacheTest + new RedirectPolicyTest): same-origin redirect keeps Authorization and X-Api-Key; cross-origin drops both, also after the chain comes back; a redirected revalidation sends the ETag and keeps the copy on 304; 10 redirects followed, the 11th fails with network-error even with allowStale; a redirect that is not followed is http-error 302; cancelling mid-chain stores nothing. The https cases are pure RedirectPolicy tests (the test server has no TLS). Full suite: 310 passed.
    • Run on the unfixed code first: the cross-origin and the 11-hop tests failed. The other four passed already, because the JVM runs the JDK's HttpURLConnection (which also drops Authorization across hosts and refuses scheme changes), not AOSP's OkHttp. With instanceFollowRedirects = false both return the 3xx, so the new tests exercise our loop.
  • iOS (not committed: there is no iOS test target): a harness compiling the real DocumentCache / SharedDownload / RedirectPolicy with stand-ins for the Nitro types, run on the iOS 26.5 simulator against a local server with two http ports and one https port (test root certificate added to the simulator's keychain). 12 cases: same origin, cross origin, chain coming back, http → https, https → http (http-error 302, nothing sent to http, no stale copy), 10 vs 11 redirects, 304 after a redirect, plus the pure policy cases. Unfixed code: 10 of 12 failed, as described in the issue. Fixed: all pass.
  • e2e: viewer-redirects passes on Android (API 37 emulator) and iOS (26.5 simulator); it ran on the fixed build only (the red runs are the JVM tests and the harness). The flows that download remotely still pass on both: viewer-offline, viewer-errors, viewer-sources, viewer-initial, pdf-document. These device runs came before a last cleanup of Android's SharedDownload.download() (the connection is now closed in one finally, no behavior change); the JVM suite was re-run after it.
  • bun run typecheck, bun run lint, bun run test (124) pass.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Remote downloads followed redirects with each platform's defaults, which
disagreed and leaked credentials. Android's HttpURLConnection (AOSP's
OkHttp) refused http → https and https → http redirects (http-error with
the 3xx) and sent every source header but Authorization on to other
hosts. iOS's URLSession followed https → http, also sent every header but
Authorization to other hosts, and dropped Authorization on every redirect,
so token-protected sources failed with 401 after a same-origin redirect.

Both platforms now follow one RedirectPolicy (Kotlin and Swift):

- at most 10 redirects; more fail with network-error;
- http → https is followed, https → http (or a target that is not
  http(s)) is not: http-error with the redirect's status;
- the source's headers, Authorization included, go only while every hop
  stays on the URL's origin (scheme, host and port); after the first hop
  to another origin, the rest of the chain is sent without them.
  If-None-Match and If-Modified-Since go on every hop, so a 304 from the
  target still validates the stored copy;
- a refused redirect and too many redirects never show the stored copy:
  the server answered.

Android follows redirects itself (instanceFollowRedirects = false, one
connection per hop, the live one reachable by cancel()). iOS rebuilds each
redirected request in a per-task URLSessionTaskDelegate, because the
request URLSession proposes has already lost Authorization.

The headers TSDoc, the sources, offline, troubleshooting and migration
guides, ARCHITECTURE.md and api-design.md describe the policy. The fixture
server gains /__redirect, /__redirect-loop and ?require= / ?forbid= header
checks, and viewer-redirects covers a same-origin redirect, a cross-origin
one (localhost → 127.0.0.1) and a loop on both platforms.

Behavior change: an http URL that redirects to https on the same host
changes origin, so its headers are not sent to the https URL. Send
credentials to the https URL directly.

Closes #70
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 4877efcb-c169-405f-90cc-44c9c15792ad
📥 Commits

Reviewing files that changed from the base of the PR and between 7d5f16f and 90ce94c.

📒 Files selected for processing: 19
  • ARCHITECTURE.md
  • README.md
  • benchmarks/serve-fixtures.mjs
  • docs/api-design.md
  • docs/api.md
  • docs/guides/offline-and-caching.md
  • docs/guides/sources-and-loading.md
  • docs/migrating-from-react-native-pdf.md
  • docs/troubleshooting.md
  • example/documents.ts
  • example/e2e/run.sh
  • example/e2e/viewer-redirects.yaml
  • package/android/src/main/java/com/margelo/nitro/betterpdf/RedirectPolicy.kt
  • package/android/src/main/java/com/margelo/nitro/betterpdf/SharedDownload.kt
  • package/android/src/test/java/com/margelo/nitro/betterpdf/DocumentCacheTest.kt
  • package/android/src/test/java/com/margelo/nitro/betterpdf/RedirectPolicyTest.kt
  • package/ios/RedirectPolicy.swift
  • package/ios/SharedDownload.swift
  • package/src/source.ts

Included review availability: This review used your included allowance. 3 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.

📝 Summary

Summary by CodeRabbit

  • New Features

    • Added consistent redirect handling for document downloads on Android and iOS.
    • Follows up to 10 HTTP(S) redirects, while allowing HTTP-to-HTTPS upgrades.
    • Protects request headers by removing them after cross-origin redirects.
    • Preserves conditional caching headers across redirects.
  • Bug Fixes

    • HTTPS-to-HTTP downgrades, unsupported redirects, redirect loops, and excessive redirects now fail with clear errors.
    • Redirect failures no longer fall back to cached copies.
  • Documentation

    • Added redirect behavior, error handling, offline caching, migration, and troubleshooting guidance.

Android and iOS downloads now follow a documented redirect policy. The policy limits redirect chains to 10, rejects HTTPS-to-HTTP downgrades, and removes caller headers after an origin change while retaining conditional headers. Tests and example flows cover redirect handling.

Changes: Download redirects

Layer / File(s) Summary
Redirect rules
package/android/src/main/java/com/margelo/nitro/betterpdf/RedirectPolicy.kt, package/ios/RedirectPolicy.swift, package/android/src/test/java/com/margelo/nitro/betterpdf/RedirectPolicyTest.kt
Added platform redirect policies with a 10-hop limit, HTTP(S) checks, HTTPS downgrade refusal, and origin-based caller-header handling. Android unit tests cover URL resolution and policy cases.
Platform download handling
package/android/src/main/java/com/margelo/nitro/betterpdf/SharedDownload.kt, package/ios/SharedDownload.swift, package/android/src/test/java/com/margelo/nitro/betterpdf/DocumentCacheTest.kt
Downloaders now handle redirects according to policy. Android cache tests cover same-origin and cross-origin headers, conditional validation, redirect limits, refused redirects, and cancellation.
Redirect fixtures and documentation
benchmarks/serve-fixtures.mjs, example/documents.ts, example/e2e/*, ARCHITECTURE.md, README.md, docs/*, package/src/source.ts
Added redirect fixture endpoints and an end-to-end viewer flow. Documentation describes redirect limits, errors, header forwarding, and cache behavior.

Priority: ⬆️ High

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: High

Merge Risk: ⚪ Minimal · up to 90ce9

The reviewed redirect behavior has no identified merge-blocking issue. It is ready to merge after normal checks.

Pre-merge checks: Passed 5 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 19.35% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 10 files. (9 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 5 passed
Check name Status Explanation
Title check Passed The title accurately describes the redirect-header change and uses the required fix: prefix. It exceeds the recommended 50-character length at 66 characters but remains concise and descriptive.
Description check Passed The description directly explains the unified redirect policy, header handling, platform changes, documentation, fixtures, and test results.
Linked Issues check Passed The PR satisfies the coding requirements in #70. Android and iOS use a 10-hop RedirectPolicy. Same-origin redirects retain caller headers, including Authorization. Cross-origin redirects remove caller…
Out of Scope Changes check Passed The changes stay within #70. The platform implementations, tests, redirect fixtures, e2e flow, API documentation, migration guidance, troubleshooting guidance, and architecture documentation all suppo…
Security Check Passed No medium-or-higher security vulnerability was introduced. Android disables automatic redirects and applies RedirectPolicy before each hop; it permits only HTTP(S), rejects HTTPS-to-HTTP, caps the c…
Full details: Linked Issues check

Explanation

The PR satisfies the coding requirements in #70. Android and iOS use a 10-hop RedirectPolicy. Same-origin redirects retain caller headers, including Authorization. Cross-origin redirects remove caller headers for the remaining chain. HTTP-to-HTTPS redirects are followed, HTTPS-to-HTTP redirects return http-error, and redirect-limit failures return network-error without stale fallback. Conditional headers persist across hops. The PR adds JVM redirect tests, cross-platform e2e coverage, fixture support, and the required TSDoc and guide updates.

Full details: Out of Scope Changes check

Explanation

The changes stay within #70. The platform implementations, tests, redirect fixtures, e2e flow, API documentation, migration guidance, troubleshooting guidance, and architecture documentation all support the unified redirect policy. No unrelated product behavior or independent feature is demonstrated.

Full details: Security Check

Explanation

No medium-or-higher security vulnerability was introduced. Android disables automatic redirects and applies RedirectPolicy before each hop; it permits only HTTP(S), rejects HTTPS-to-HTTP, caps the chain at 10, and permanently disables caller headers after any scheme, host, or port change. iOS rebuilds each redirected request with the same policy. Both implementations retain only conditional cache validators across hops, as documented, and source URL/header validation remains in place. The changed fixture code is test-only. No prior security findings or unresolved threads were supplied.

Full details: Docstring Coverage

Explanation

Docstring coverage is 19.35% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 10 files. (9 skipped: 9 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.

Comment @coderabbitai help to get the list of available commands.

@jkasprzyk17
jkasprzyk17 merged commit 44fc170 into main Oct 21, 2026
5 checks passed
@jkasprzyk17
jkasprzyk17 deleted the fix/redirect-policy branch October 21, 2026 11:08
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.

Redirects: source.headers are sent on to other hosts, Android refuses http→https, and none of it is documented

1 participant