From c343524a1b7b9e8fa1d33e36b943ec8af31fd753 Mon Sep 17 00:00:00 2001 From: Ofer Morag Date: Sat, 11 Apr 2026 09:31:39 +0300 Subject: [PATCH 1/5] fix(android): sync Cronet and token-refresh requests with CookieManager - NitroFetchClient: attach Cookie from CookieManager when request has no Cookie header; persist Set-Cookie from responses (including redirects). - AutoPrefetcher: same for HttpURLConnection token refresh; persist Set-Cookie from refresh response. Helps SAML/session flows where the session cookie lives in the WebView cookie jar. Made-with: Cursor --- .../nitro/nitrofetch/AutoPrefetcher.kt | 34 ++++++++++++++++++- .../nitro/nitrofetch/NitroFetchClient.kt | 34 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt index 9eb6e10..6555079 100644 --- a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +++ b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt @@ -2,6 +2,7 @@ package com.margelo.nitro.nitrofetch import android.app.Application import android.content.Context +import android.webkit.CookieManager import org.json.JSONArray import org.json.JSONObject import java.net.HttpURLConnection @@ -151,16 +152,47 @@ object AutoPrefetcher { conn.doInput = true if (body != null) conn.doOutput = true + var hasCookieHeader = false reqHeaders?.keys()?.forEachRemaining { k -> + if (k.equals("Cookie", ignoreCase = true)) hasCookieHeader = true conn.setRequestProperty(k, reqHeaders.optString(k, "")) } + if (!hasCookieHeader) { + try { + val jar = CookieManager.getInstance() + val cookieHeader = jar.getCookie(urlStr) + if (!cookieHeader.isNullOrEmpty()) { + conn.setRequestProperty("Cookie", cookieHeader) + } + } catch (_: Throwable) { + // Best-effort — CookieManager may not be initialized yet + } + } + if (body != null) { conn.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) } } val status = conn.responseCode - if (status !in 200..299) return null + if (status !in 200..299) { + android.util.Log.d("NitroFetch", "[TokenRefresh] Refresh endpoint returned HTTP $status") + return null + } + + try { + val cookieManager = CookieManager.getInstance() + conn.headerFields?.forEach { (key, values) -> + if (key?.equals("Set-Cookie", ignoreCase = true) == true) { + values.forEach { cookieValue -> + cookieManager.setCookie(urlStr, cookieValue) + } + } + } + cookieManager.flush() + } catch (_: Throwable) { + // Best-effort — CookieManager may not be initialized yet + } val responseBody = conn.inputStream.use { it.bufferedReader(Charsets.UTF_8).readText() } diff --git a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt index a6fb46a..61becea 100644 --- a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +++ b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt @@ -3,6 +3,7 @@ package com.margelo.nitro.nitrofetch import android.net.Uri import android.os.Trace import android.util.Log +import android.webkit.CookieManager import com.facebook.proguard.annotations.DoNotStrip import com.margelo.nitro.NitroModules import com.margelo.nitro.core.ArrayBuffer @@ -48,6 +49,25 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E } companion object { + private fun hasCookieHeader(request: NitroRequest): Boolean { + return request.headers?.any { it.key.equals("Cookie", ignoreCase = true) } == true + } + + private fun storeResponseCookies(responseUrl: String, info: UrlResponseInfo) { + try { + val cookieManager = CookieManager.getInstance() + val setCookieHeaders = info.allHeadersAsList.filter { + it.key.equals("Set-Cookie", ignoreCase = true) + } + for (header in setCookieHeaders) { + cookieManager.setCookie(responseUrl, header.value) + } + cookieManager.flush() + } catch (exception: Exception) { + Log.w("NitroFetchClient", "Failed to store response cookies", exception) + } + } + @JvmStatic fun fetch( req: NitroRequest, @@ -87,6 +107,7 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) { if (shouldFollowRedirects) { + storeResponseCookies(info.url, info) request.followRedirect() } else { // Return the redirect response as-is without following @@ -131,6 +152,7 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E Trace.endAsyncSection(traceLabel, traceCookie) } try { + storeResponseCookies(info.url, info) val headersArr: Array = info.allHeadersAsList.map { NitroHeader(it.key, it.value) }.toTypedArray() val status = info.httpStatusCode @@ -184,6 +206,18 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E builder.setHttpMethod(method) req.headers?.forEach { (k, v) -> builder.addHeader(k, v) } + if (!hasCookieHeader(req)) { + try { + val cookieManager = CookieManager.getInstance() + val cookie = cookieManager.getCookie(url) + if (!cookie.isNullOrEmpty()) { + builder.addHeader("Cookie", cookie) + } + } catch (exception: Exception) { + Log.w("NitroFetchClient", "Failed to attach cookie header", exception) + } + } + val formParts = req.bodyFormData if (formParts != null && formParts.isNotEmpty()) { val (multipartBody, contentType) = buildMultipartBody(formParts) From 7d4c6840c9d3155b88cc37e176ec6a3cb49fabbe Mon Sep 17 00:00:00 2001 From: Ofer Morag Date: Sun, 12 Apr 2026 11:02:03 +0300 Subject: [PATCH 2/5] fix(android): address cookie sync review (shared helper, flush once) - NitroCookieSync: shared attach + Set-Cookie helpers; flush only when cookies applied - Cronet: apply Set-Cookie on redirects without flush; flush once on success when redirects or final response stored cookies - HttpURLConnection token refresh: reuse attach helper; Set-Cookie + flush only if needed Made-with: Cursor --- .../nitro/nitrofetch/AutoPrefetcher.kt | 32 +---- .../nitro/nitrofetch/NitroCookieSync.kt | 114 ++++++++++++++++++ .../nitro/nitrofetch/NitroFetchClient.kt | 48 +++----- 3 files changed, 134 insertions(+), 60 deletions(-) create mode 100644 packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroCookieSync.kt diff --git a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt index 6555079..072e403 100644 --- a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +++ b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt @@ -2,7 +2,6 @@ package com.margelo.nitro.nitrofetch import android.app.Application import android.content.Context -import android.webkit.CookieManager import org.json.JSONArray import org.json.JSONObject import java.net.HttpURLConnection @@ -152,23 +151,14 @@ object AutoPrefetcher { conn.doInput = true if (body != null) conn.doOutput = true - var hasCookieHeader = false reqHeaders?.keys()?.forEachRemaining { k -> - if (k.equals("Cookie", ignoreCase = true)) hasCookieHeader = true conn.setRequestProperty(k, reqHeaders.optString(k, "")) } - if (!hasCookieHeader) { - try { - val jar = CookieManager.getInstance() - val cookieHeader = jar.getCookie(urlStr) - if (!cookieHeader.isNullOrEmpty()) { - conn.setRequestProperty("Cookie", cookieHeader) - } - } catch (_: Throwable) { - // Best-effort — CookieManager may not be initialized yet - } - } + NitroCookieSync.attachCookieFromManagerIfMissing( + urlStr, + NitroCookieSync.hasCookieHeaderInJson(reqHeaders) + ) { key, value -> conn.setRequestProperty(key, value) } if (body != null) { conn.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) } @@ -180,19 +170,7 @@ object AutoPrefetcher { return null } - try { - val cookieManager = CookieManager.getInstance() - conn.headerFields?.forEach { (key, values) -> - if (key?.equals("Set-Cookie", ignoreCase = true) == true) { - values.forEach { cookieValue -> - cookieManager.setCookie(urlStr, cookieValue) - } - } - } - cookieManager.flush() - } catch (_: Throwable) { - // Best-effort — CookieManager may not be initialized yet - } + NitroCookieSync.storeSetCookieFromHttpURLConnection(urlStr, conn, flush = true) val responseBody = conn.inputStream.use { it.bufferedReader(Charsets.UTF_8).readText() } diff --git a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroCookieSync.kt b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroCookieSync.kt new file mode 100644 index 0000000..9abfc1f --- /dev/null +++ b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroCookieSync.kt @@ -0,0 +1,114 @@ +package com.margelo.nitro.nitrofetch + +import android.util.Log +import android.webkit.CookieManager +import org.json.JSONObject +import org.chromium.net.UrlResponseInfo +import java.net.HttpURLConnection + +/** + * Shared [CookieManager] bridging for Cronet and [HttpURLConnection] token refresh. + * - Attaches `Cookie` from the jar when the request has no `Cookie` header. + * - Persists `Set-Cookie` responses; [flush] is applied only when at least one cookie was stored. + */ +internal object NitroCookieSync { + private const val LOG_TAG = "NitroCookieSync" + + fun hasCookieHeaderInNitroRequest(headers: Array?): Boolean { + return headers?.any { it.key.equals("Cookie", ignoreCase = true) } == true + } + + fun hasCookieHeaderInJson(reqHeaders: JSONObject?): Boolean { + if (reqHeaders == null) return false + return reqHeaders.keys().asSequence().any { it.equals("Cookie", ignoreCase = true) } + } + + /** + * If [hasCookieHeader] is false, adds `Cookie` from [CookieManager] for [url] via [addHeader]. + */ + fun attachCookieFromManagerIfMissing( + url: String, + hasCookieHeader: Boolean, + addHeader: (String, String) -> Unit + ) { + if (hasCookieHeader) return + try { + val jar = CookieManager.getInstance() + val cookieHeader = jar.getCookie(url) + if (!cookieHeader.isNullOrEmpty()) { + addHeader("Cookie", cookieHeader) + } + } catch (exception: Exception) { + Log.w(LOG_TAG, "Failed to attach cookie header", exception) + } + } + + /** + * Applies `Set-Cookie` headers from a Cronet [UrlResponseInfo] into [CookieManager]. + * @param flush If true, [CookieManager.flush] runs only when at least one cookie was applied. + * Use `flush = false` on redirects so persistence happens once on the final response. + * @return true if at least one `Set-Cookie` was stored. + */ + fun storeSetCookieFromUrlResponseInfo( + responseUrl: String, + info: UrlResponseInfo, + flush: Boolean + ): Boolean { + return try { + val cookieManager = CookieManager.getInstance() + val setCookieHeaders = info.allHeadersAsList.filter { + it.key.equals("Set-Cookie", ignoreCase = true) + } + if (setCookieHeaders.isEmpty()) return false + for (header in setCookieHeaders) { + cookieManager.setCookie(responseUrl, header.value) + } + if (flush) { + cookieManager.flush() + } + true + } catch (exception: Exception) { + Log.w(LOG_TAG, "Failed to store response cookies", exception) + false + } + } + + /** + * Applies `Set-Cookie` from an [HttpURLConnection] response into [CookieManager]. + * @param flush If true, [CookieManager.flush] runs only when at least one cookie was applied. + */ + fun storeSetCookieFromHttpURLConnection( + urlStr: String, + conn: HttpURLConnection, + flush: Boolean + ): Boolean { + return try { + val cookieManager = CookieManager.getInstance() + var anySet = false + conn.headerFields?.forEach { (key, values) -> + if (key?.equals("Set-Cookie", ignoreCase = true) == true) { + values.forEach { cookieValue -> + cookieManager.setCookie(urlStr, cookieValue) + anySet = true + } + } + } + if (anySet && flush) { + cookieManager.flush() + } + anySet + } catch (exception: Exception) { + Log.w(LOG_TAG, "Failed to store response cookies (HttpURLConnection)", exception) + false + } + } + + /** Persists in-memory cookie updates to disk (call after a successful request when any `Set-Cookie` was applied). */ + fun flushCookieManager() { + try { + CookieManager.getInstance().flush() + } catch (exception: Exception) { + Log.w(LOG_TAG, "Failed to flush CookieManager", exception) + } + } +} diff --git a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt index 61becea..3ea47c0 100644 --- a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +++ b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt @@ -3,7 +3,6 @@ package com.margelo.nitro.nitrofetch import android.net.Uri import android.os.Trace import android.util.Log -import android.webkit.CookieManager import com.facebook.proguard.annotations.DoNotStrip import com.margelo.nitro.NitroModules import com.margelo.nitro.core.ArrayBuffer @@ -49,25 +48,6 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E } companion object { - private fun hasCookieHeader(request: NitroRequest): Boolean { - return request.headers?.any { it.key.equals("Cookie", ignoreCase = true) } == true - } - - private fun storeResponseCookies(responseUrl: String, info: UrlResponseInfo) { - try { - val cookieManager = CookieManager.getInstance() - val setCookieHeaders = info.allHeadersAsList.filter { - it.key.equals("Set-Cookie", ignoreCase = true) - } - for (header in setCookieHeaders) { - cookieManager.setCookie(responseUrl, header.value) - } - cookieManager.flush() - } catch (exception: Exception) { - Log.w("NitroFetchClient", "Failed to store response cookies", exception) - } - } - @JvmStatic fun fetch( req: NitroRequest, @@ -104,10 +84,15 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E private val buffer = ByteBuffer.allocateDirect(16 * 1024) private val out = java.io.ByteArrayOutputStream() private var redirectStopped = false + /** True if a redirect response applied at least one `Set-Cookie` (in memory, not yet flushed). */ + private var setCookieAppliedOnRedirect = false override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) { if (shouldFollowRedirects) { - storeResponseCookies(info.url, info) + // Apply Set-Cookie in-memory; flush once in onSucceeded (avoid flush per hop). + if (NitroCookieSync.storeSetCookieFromUrlResponseInfo(info.url, info, flush = false)) { + setCookieAppliedOnRedirect = true + } request.followRedirect() } else { // Return the redirect response as-is without following @@ -152,7 +137,11 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E Trace.endAsyncSection(traceLabel, traceCookie) } try { - storeResponseCookies(info.url, info) + val storedOnFinal = + NitroCookieSync.storeSetCookieFromUrlResponseInfo(info.url, info, flush = false) + if (storedOnFinal || setCookieAppliedOnRedirect) { + NitroCookieSync.flushCookieManager() + } val headersArr: Array = info.allHeadersAsList.map { NitroHeader(it.key, it.value) }.toTypedArray() val status = info.httpStatusCode @@ -206,17 +195,10 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E builder.setHttpMethod(method) req.headers?.forEach { (k, v) -> builder.addHeader(k, v) } - if (!hasCookieHeader(req)) { - try { - val cookieManager = CookieManager.getInstance() - val cookie = cookieManager.getCookie(url) - if (!cookie.isNullOrEmpty()) { - builder.addHeader("Cookie", cookie) - } - } catch (exception: Exception) { - Log.w("NitroFetchClient", "Failed to attach cookie header", exception) - } - } + NitroCookieSync.attachCookieFromManagerIfMissing( + url, + NitroCookieSync.hasCookieHeaderInNitroRequest(req.headers) + ) { key, value -> builder.addHeader(key, value) } val formParts = req.bodyFormData if (formParts != null && formParts.isNotEmpty()) { From 6a26d1143713e65333b6b7bb3b489529eea4c818 Mon Sep 17 00:00:00 2001 From: Ofer Morag Date: Sun, 12 Apr 2026 11:22:12 +0300 Subject: [PATCH 3/5] fix(android): use final URL for cookie storage after redirects HttpURLConnection follows redirects by default; conn.url returns the final URL, which is the correct domain to associate Set-Cookie with. Made-with: Cursor --- .../main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt index 072e403..e0e9a69 100644 --- a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +++ b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt @@ -170,7 +170,7 @@ object AutoPrefetcher { return null } - NitroCookieSync.storeSetCookieFromHttpURLConnection(urlStr, conn, flush = true) + NitroCookieSync.storeSetCookieFromHttpURLConnection(conn.url.toString(), conn, flush = true) val responseBody = conn.inputStream.use { it.bufferedReader(Charsets.UTF_8).readText() } From 25f5968601fa8fad8733f2de9ac641564d1b0679 Mon Sep 17 00:00:00 2001 From: Ofer Morag Date: Mon, 13 Apr 2026 23:26:43 +0300 Subject: [PATCH 4/5] fix(android): make cookie sync opt-in via NitroCookieSync.enableCookieSync() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cookie sync is now disabled by default — requests that were previously anonymous stay that way unless the consumer explicitly calls NitroCookieSync.enableCookieSync() from Application.onCreate. All public methods (attach, store, flush) short-circuit with a no-op when disabled. Addresses: https://github.com/margelo/react-native-nitro-fetch/pull/73#issuecomment-4238036603 Made-with: Cursor --- .../nitro/nitrofetch/NitroCookieSync.kt | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroCookieSync.kt b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroCookieSync.kt index 9abfc1f..eb2d3a0 100644 --- a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroCookieSync.kt +++ b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroCookieSync.kt @@ -10,10 +10,29 @@ import java.net.HttpURLConnection * Shared [CookieManager] bridging for Cronet and [HttpURLConnection] token refresh. * - Attaches `Cookie` from the jar when the request has no `Cookie` header. * - Persists `Set-Cookie` responses; [flush] is applied only when at least one cookie was stored. + * + * **Opt-in:** Cookie sync is disabled by default to avoid changing behaviour for consumers + * that do not rely on the WebView cookie jar. Call [enableCookieSync] before any requests. */ -internal object NitroCookieSync { +object NitroCookieSync { private const val LOG_TAG = "NitroCookieSync" + @Volatile + private var enabled = false + + /** + * Enable cookie synchronisation between Cronet / HttpURLConnection and the system + * [CookieManager]. Call once (e.g. from `Application.onCreate`) before any fetch or + * autoprefetch work. Has no effect when called multiple times. + */ + @JvmStatic + fun enableCookieSync() { + enabled = true + } + + @JvmStatic + fun isCookieSyncEnabled(): Boolean = enabled + fun hasCookieHeaderInNitroRequest(headers: Array?): Boolean { return headers?.any { it.key.equals("Cookie", ignoreCase = true) } == true } @@ -25,12 +44,14 @@ internal object NitroCookieSync { /** * If [hasCookieHeader] is false, adds `Cookie` from [CookieManager] for [url] via [addHeader]. + * No-op when cookie sync is [disabled][enableCookieSync]. */ fun attachCookieFromManagerIfMissing( url: String, hasCookieHeader: Boolean, addHeader: (String, String) -> Unit ) { + if (!enabled) return if (hasCookieHeader) return try { val jar = CookieManager.getInstance() @@ -54,6 +75,7 @@ internal object NitroCookieSync { info: UrlResponseInfo, flush: Boolean ): Boolean { + if (!enabled) return false return try { val cookieManager = CookieManager.getInstance() val setCookieHeaders = info.allHeadersAsList.filter { @@ -82,6 +104,7 @@ internal object NitroCookieSync { conn: HttpURLConnection, flush: Boolean ): Boolean { + if (!enabled) return false return try { val cookieManager = CookieManager.getInstance() var anySet = false @@ -105,6 +128,7 @@ internal object NitroCookieSync { /** Persists in-memory cookie updates to disk (call after a successful request when any `Set-Cookie` was applied). */ fun flushCookieManager() { + if (!enabled) return try { CookieManager.getInstance().flush() } catch (exception: Exception) { From 07a1a5cd024d84e7f2ec5f9b8591b597644073cf Mon Sep 17 00:00:00 2001 From: riteshshukla04 Date: Mon, 4 May 2026 20:15:15 +0530 Subject: [PATCH 5/5] chore: docs+ minor nites --- docs-website/docs/cookie-sync.md | 40 ++++++++++++++++++++++++++++++++ docs-website/sidebars.ts | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 docs-website/docs/cookie-sync.md diff --git a/docs-website/docs/cookie-sync.md b/docs-website/docs/cookie-sync.md new file mode 100644 index 0000000..365d075 --- /dev/null +++ b/docs-website/docs/cookie-sync.md @@ -0,0 +1,40 @@ +--- +id: cookie-sync +title: Cookie Sync (Android) +sidebar_position: 12 +--- + +# Cookie Sync (Android) + +Bridges Android's WebView [`CookieManager`](https://developer.android.com/reference/android/webkit/CookieManager) with `nitro-fetch`'s Cronet client and the cold-start token-refresh path. Useful when your auth flow stores the session cookie in the WebView cookie jar (e.g. SAML, OAuth login pages rendered in a WebView) and you need subsequent native fetches to send it. + +When enabled: + +- **Outbound requests**: if the request has no `Cookie` header, the matching cookies from `CookieManager` are attached for the request URL. +- **Inbound responses**: any `Set-Cookie` headers (including those returned during redirects) are stored back into `CookieManager`. Persistence is flushed once per request after the final response. + +User-set `Cookie` headers are always respected — sync never overwrites them. + +## Enable + +Cookie sync is **opt-in** and disabled by default. Enable it once from your `Application.onCreate()` (or any code path that runs before the first fetch / auto-prefetch): + +```kotlin +// android/app/src/main/java/.../MainApplication.kt +import com.margelo.nitro.nitrofetch.NitroCookieSync + +class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() + NitroCookieSync.enableCookieSync() + // ...rest of your onCreate + } +} +``` + +That's it — both the Cronet client (`fetch`) and the `HttpURLConnection` token-refresh path will start syncing cookies on the next request. + +## Notes + +- **Android only.** iOS `URLSession` already shares cookies with `WKWebView` via `HTTPCookieStorage` and needs no opt-in. +- **Token-refresh redirects**: `HttpURLConnection` follows redirects internally and only the final response's `Set-Cookie` headers are visible. If your refresh endpoint sets cookies on a 3xx hop, point it directly at the final URL. diff --git a/docs-website/sidebars.ts b/docs-website/sidebars.ts index 4cdc1da..69cc245 100644 --- a/docs-website/sidebars.ts +++ b/docs-website/sidebars.ts @@ -20,7 +20,7 @@ const sidebars: SidebarsConfig = { { type: 'category', label: 'Advanced', - items: ['worklets', 'inspection', 'global-replace'], + items: ['worklets', 'inspection', 'global-replace', 'cookie-sync'], }, 'skills', 'troubleshooting',