Fix Android WebSocket cookie lookup stripping the URL path (#58365)

Summary:
`WebSocketModule.getCookie` looks up cookies through `getDefaultOrigin(uri)`, which strips the URL down to `scheme://host[:port]` before handing it to `ForwardingCookieHandler`. Android's `CookieManager` matches cookies against the full URL (domain and path), so any cookie set with a `Path` other than `/` gets silently dropped from the WebSocket handshake, breaking auth/session cookies scoped to a sub-path (e.g. `/signal-r/hubs/messages`).

`getDefaultOrigin` is also used to build the `origin` header for the handshake, where stripping the path is correct per the WebSocket protocol, so it can't just be changed in place without affecting that header too. This adds a separate `getCookieLookupUri` that does the same `ws(s)://` to `http(s)://` scheme mapping but keeps the path, query, and fragment, and uses it only for the cookie lookup. It also drops the URI's userinfo from that lookup, since it plays no role in cookie matching and shouldn't be forwarded into the CookieManager call.

## Changelog:

[ANDROID] [FIXED] - Fix WebSocket cookie lookup dropping path-scoped cookies

Pull Request resolved: https://github.com/react/react-native/pull/58365

Test Plan:
Added `WebSocketModuleTest`, exercising `getCookieLookupUri` through reflection and asserting the path, port, and query survive the ws/wss -> http/https conversion. Couldn't run it through the repo's own Gradle/Robolectric setup in this environment (`react-native-gradle-plugin` isn't resolvable without the full monorepo build), so I compiled the real companion object with `kotlinc` standalone and ran the same reflection lookup against it directly, confirming both cases pass and that the private companion method resolves without a `NoSuchMethodException`.

Fixes https://github.com/react/react-native/issues/58358

Reviewed By: cipolleschi

Differential Revision: D119093059

Pulled By: javache

fbshipit-source-id: c5404937c97b497d3eae8c45433785f057602c29
This commit is contained in:
Lazizbek Ergashev
2026-09-08 04:43:42 -07:00
committed by meta-codesync[bot]
parent 07a6e1fc41
commit 0eb9b300b6
2 changed files with 79 additions and 9 deletions
@@ -427,7 +427,7 @@ public class WebSocketModule(context: ReactApplicationContext) :
*/
private fun getCookie(uri: String): String? {
try {
val origin = URI(getDefaultOrigin(uri))
val origin = getCookieLookupUri(uri)
val cookieMap = cookieHandler.get(origin, HashMap<String, List<String>>())
val cookieList = cookieMap["Cookie"]
if (cookieList.isNullOrEmpty()) {
@@ -459,6 +459,16 @@ public class WebSocketModule(context: ReactApplicationContext) :
customClientBuilder?.apply(builder)
}
/** Map a WebSocket URI's scheme to its HTTP(S) equivalent, e.g. "wss" -> "https". */
private fun httpSchemeFor(requestURI: URI): String =
when (requestURI.scheme) {
"wss" -> "https"
"ws" -> "http"
"http",
"https" -> requestURI.scheme
else -> ""
}
/**
* Get the default HTTP(S) origin for a specific WebSocket URI
*
@@ -468,14 +478,7 @@ public class WebSocketModule(context: ReactApplicationContext) :
private fun getDefaultOrigin(uri: String): String {
try {
val requestURI = URI(uri)
val scheme =
when (requestURI.scheme) {
"wss" -> "https"
"ws" -> "http"
"http",
"https" -> requestURI.scheme
else -> ""
}
val scheme = httpSchemeFor(requestURI)
val defaultOrigin =
if (requestURI.port != -1) {
@@ -489,5 +492,31 @@ public class WebSocketModule(context: ReactApplicationContext) :
throw IllegalArgumentException("Unable to set $uri as default origin header")
}
}
/**
* Get the URI used to look up cookies for a specific WebSocket URI, keeping its path so that
* path-scoped cookies are matched correctly. Query and fragment are dropped since cookies are
* scoped by path, not by query or fragment (RFC 6265). userInfo is also dropped so that
* credentials embedded in the URL are never forwarded to the cookie store.
*
* @param uri
* @return A URI with the endpoint converted to HTTP protocol (http[s]://host[:port]/path)
*/
private fun getCookieLookupUri(uri: String): URI {
try {
val requestURI = URI(uri)
return URI(
httpSchemeFor(requestURI),
null,
requestURI.host,
requestURI.port,
requestURI.path,
null,
null,
)
} catch (e: URISyntaxException) {
throw IllegalArgumentException("Unable to get cookie lookup URI from $uri")
}
}
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.modules.websocket
import java.net.URI
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class WebSocketModuleTest {
private fun getCookieLookupUri(uri: String): URI {
val method =
WebSocketModule.Companion::class
.java
.getDeclaredMethod(
"getCookieLookupUri",
String::class.java,
)
method.isAccessible = true
return method.invoke(WebSocketModule.Companion, uri) as URI
}
@Test
fun getCookieLookupUri_keepsPathForCookieMatching() {
val uri = getCookieLookupUri("wss://my.domain/signal-r/hubs/messages")
assertThat(uri.toString()).isEqualTo("https://my.domain/signal-r/hubs/messages")
}
@Test
fun getCookieLookupUri_keepsPortAndDropsQuery() {
val uri = getCookieLookupUri("ws://my.domain:8080/path?token=abc")
assertThat(uri.toString()).isEqualTo("http://my.domain:8080/path")
}
}