Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ object HotwireHttpClient {

private fun buildNewHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.cookieJar(WebViewCookieJar())
.connectTimeout(10L, TimeUnit.SECONDS)
.readTimeout(30L, TimeUnit.SECONDS)
.writeTimeout(30L, TimeUnit.SECONDS)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dev.hotwire.core.turbo.http

import android.webkit.CookieManager
import dev.hotwire.core.logging.logError
import dev.hotwire.core.turbo.util.dispatcherProvider
import kotlinx.coroutines.withContext
Expand All @@ -9,7 +8,6 @@ import okhttp3.Request
import okhttp3.Response

internal class HttpRepository {
private val cookieManager = CookieManager.getInstance()

data class HttpRequestResult(
val response: Response,
Expand Down Expand Up @@ -54,12 +52,6 @@ internal class HttpRepository {
}

private fun buildRequest(location: String): Request {
val builder = Request.Builder().url(location)

cookieManager.getCookie(location)?.let {
builder.header("Cookie", it)
}

return builder.build()
return Request.Builder().url(location).build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.hotwire.core.turbo.http

import android.webkit.CookieManager
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl

internal class WebViewCookieJar : CookieJar {
private val cookieManager = CookieManager.getInstance()

override fun loadForRequest(url: HttpUrl): List<Cookie> {
val header = cookieManager.getCookie(url.toString()) ?: return emptyList()
return header.split("; ").mapNotNull { Cookie.parse(url, it) }
}

override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
cookies.forEach { cookieManager.setCookie(url.toString(), it.toString()) }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dev.hotwire.core.turbo.offline

import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import dev.hotwire.core.logging.logError
Expand All @@ -22,7 +21,6 @@ import java.io.InputStream
* Experimental: API may change, not ready for production use.
*/
internal class OfflineHttpRepository(private val coroutineScope: CoroutineScope) {
private val cookieManager = CookieManager.getInstance()

// Limit pre-cache requests to 2 concurrently
private val preCacheRequestQueue = Semaphore(2)
Expand Down Expand Up @@ -116,36 +114,18 @@ internal class OfflineHttpRepository(private val coroutineScope: CoroutineScope)
val headers = resourceRequest.requestHeaders
val builder = Request.Builder().url(location)

headers.forEach { builder.header(it.key, it.value) }

getCookie(location)?.let {
builder.header("Cookie", it)
}
headers
.filterKeys { !it.equals("Cookie", ignoreCase = true) }
.forEach { builder.header(it.key, it.value) }

return builder.build()
}

private fun getResponse(request: Request): Response? {
val location = request.url.toString()
val call = HotwireHttpClient.instance.newCall(request)

return call.execute().let { response ->
if (response.isSuccessful) {
setCookies(location, response)
response
} else {
null
}
}
}

private fun getCookie(location: String): String? {
return cookieManager.getCookie(location)
}

private fun setCookies(location: String, response: Response) {
response.headers("Set-Cookie").forEach {
cookieManager.setCookie(location, it)
if (response.isSuccessful) response else null
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package dev.hotwire.core.turbo.offline

import android.net.Uri
import android.webkit.CookieManager
import android.webkit.WebResourceRequest

/**
* Experimental: API may change, not ready for production use.
*/
internal class OfflinePreCacheRequest(val url: String, val userAgent: String) : WebResourceRequest {
private val cookieManager = CookieManager.getInstance()

override fun getUrl(): Uri {
return Uri.parse(url)
Expand All @@ -24,7 +22,6 @@ internal class OfflinePreCacheRequest(val url: String, val userAgent: String) :

override fun getRequestHeaders(): Map<String, String> {
return mapOf(
"Cookie" to cookieManager.getCookie(url),
"User-Agent" to userAgent
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ open class BaseRepositoryTest : BaseUnitTest() {

override fun teardown() {
super.teardown()
HotwireHttpClient.reset()
Dispatchers.resetMain()
server.shutdown()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package dev.hotwire.core.turbo.http

import android.os.Build
import android.webkit.CookieManager
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.R])
class WebViewCookieJarTest {
private val cookieJar = WebViewCookieJar()
private val cookieManager = CookieManager.getInstance()
private val server = MockWebServer()

private val greenUrl = "https://green.example.com/".toHttpUrl()
private val blueUrl = "https://blue.example.com/".toHttpUrl()

@Before
fun setup() {
cookieManager.removeAllCookies(null)
server.start()
}

@After
fun teardown() {
server.shutdown()
}

@Test
fun `the shared http client uses the web view cookie jar`() {
assertThat(HotwireHttpClient.instance.cookieJar).isInstanceOf(WebViewCookieJar::class.java)
}

@Test
fun `loadForRequest returns cookies stored for the request host`() {
cookieManager.setCookie(greenUrl.toString(), "flavor=oatmeal")

val cookies = cookieJar.loadForRequest(greenUrl)

assertThat(cookies).hasSize(1)
assertThat(cookies[0].name).isEqualTo("flavor")
assertThat(cookies[0].value).isEqualTo("oatmeal")
}

@Test
fun `loadForRequest returns all cookies stored for the request host`() {
cookieManager.setCookie(greenUrl.toString(), "flavor=oatmeal")
cookieManager.setCookie(greenUrl.toString(), "size=large")

val cookies = cookieJar.loadForRequest(greenUrl)

assertThat(cookies.map { it.name }).containsExactlyInAnyOrder("flavor", "size")
}

@Test
fun `loadForRequest returns an empty list when no cookies are stored`() {
val cookies = cookieJar.loadForRequest(greenUrl)

assertThat(cookies).isEmpty()
}

@Test
fun `loadForRequest returns only the cookies for the request host`() {
cookieManager.setCookie(greenUrl.toString(), "flavor=oatmeal")

val cookies = cookieJar.loadForRequest(blueUrl)

assertThat(cookies).isEmpty()
}

@Test
fun `saveFromResponse stores cookies for the response host`() {
val cookie = Cookie.parse(greenUrl, "flavor=oatmeal")!!

cookieJar.saveFromResponse(greenUrl, listOf(cookie))

assertThat(cookieManager.getCookie(greenUrl.toString())).contains("flavor=oatmeal")
}

@Test
fun `saveFromResponse stores cookies only for the response host`() {
val cookie = Cookie.parse(blueUrl, "flavor=oatmeal")!!

cookieJar.saveFromResponse(blueUrl, listOf(cookie))

assertThat(cookieManager.getCookie(greenUrl.toString())).isNull()
}

@Test
fun `requests include the cookies stored for their host`() {
val url = server.url("/")
cookieManager.setCookie(url.toString(), "flavor=oatmeal")
server.enqueue(MockResponse())

client().newCall(Request.Builder().url(url).build()).execute()

assertThat(server.takeRequest().getHeader("Cookie")).isEqualTo("flavor=oatmeal")
}

@Test
fun `response cookies are stored for the response host`() {
val url = server.url("/")
server.enqueue(MockResponse().addHeader("Set-Cookie", "flavor=oatmeal"))

client().newCall(Request.Builder().url(url).build()).execute()

assertThat(cookieManager.getCookie(url.toString())).contains("flavor=oatmeal")
}

@Test
fun `cookies are loaded fresh for each request when following redirects`() {
val url = server.url("/")
server.enqueue(
MockResponse()
.setResponseCode(302)
.addHeader("Location", server.url("/next"))
.addHeader("Set-Cookie", "flavor=oatmeal")
)
server.enqueue(MockResponse())

client().newCall(Request.Builder().url(url).build()).execute()

server.takeRequest()
assertThat(server.takeRequest().getHeader("Cookie")).isEqualTo("flavor=oatmeal")
}

private fun client(): OkHttpClient {
return OkHttpClient.Builder()
.cookieJar(cookieJar)
.build()
}
}
Loading