From 4e6493a600d0e338c9f0f41a5ba86f736bbc4efe Mon Sep 17 00:00:00 2001 From: Ofer Morag Date: Sat, 11 Apr 2026 09:31:39 +0300 Subject: [PATCH] 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)