diff --git a/app/src/androidTest/java/at/bitfire/icsdroid/MockServer.kt b/app/src/androidTest/java/at/bitfire/icsdroid/MockServer.kt index 6b6f99d1..1ad19f03 100644 --- a/app/src/androidTest/java/at/bitfire/icsdroid/MockServer.kt +++ b/app/src/androidTest/java/at/bitfire/icsdroid/MockServer.kt @@ -21,8 +21,14 @@ object MockServer { private val queue = mutableListOf() + var lastRequestHeaders: Headers? = null + private set + val createMockEngine: (CustomCertManager, SSLContext) -> HttpClientEngine = { _, _ -> - MockEngine { + MockEngine { request -> + // record headers from the incoming request + lastRequestHeaders = request.headers + if (queue.isNotEmpty()) { val response = lock.withLock { queue.removeAt(0) } respond(response.content, response.status, response.headers) @@ -34,6 +40,7 @@ object MockServer { fun clear() { queue.clear() + lastRequestHeaders = null } private fun enqueue(response: Response) { diff --git a/app/src/androidTest/java/at/bitfire/icsdroid/TestAppHttpClient.kt b/app/src/androidTest/java/at/bitfire/icsdroid/TestAppHttpClient.kt new file mode 100644 index 00000000..0c754711 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/icsdroid/TestAppHttpClient.kt @@ -0,0 +1,57 @@ +package at.bitfire.icsdroid + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TestAppHttpClient { + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var client: AppHttpClient + + @Before + fun setUp() { + MockServer.clear() + client = MockServer.httpClient(context) + } + + @After + fun tearDown() { + MockServer.clear() + } + + // Verifies that no Accept-Charset header is sent by default + @Test + fun request_doesNotContainAcceptCharsetHeader() = runBlocking { + // enqueue a simple 200 response + MockServer.enqueue(content = "ok", status = HttpStatusCode.OK) + + // perform a GET request to the mock server + val uri = MockServer.uri("test") + val response: HttpResponse = client.httpClient.get(uri.toString()) + + assertEquals(HttpStatusCode.OK, response.status) + + // retrieve the headers recorded by the mock server + val headers = MockServer.lastRequestHeaders + + // Ensure headers were recorded + assertNotNull(headers) + + // Assert that Accept-Charset header is not present + assertFalse(headers!!.contains(HttpHeaders.AcceptCharset)) + } +} \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt b/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt index e67ce50f..ac39c456 100644 --- a/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt +++ b/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt @@ -17,7 +17,10 @@ import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.UserAgent +import io.ktor.client.plugins.api.Send +import io.ktor.client.plugins.api.createClientPlugin import io.ktor.client.plugins.cookies.HttpCookies +import io.ktor.http.HttpHeaders import okhttp3.brotli.BrotliInterceptor import okhttp3.internal.tls.OkHostnameVerifier import javax.net.ssl.SSLContext @@ -85,6 +88,15 @@ class AppHttpClient @AssistedInject constructor( // Enable cookie storage - in memory, will be lost on app restart install(HttpCookies) + // Some servers have issues with the Accept-Charset header. It is actually deprecated/not-recommended by RFC 9110 ยง12.5.2. + // Ktor adds it by default, so we need to manually strip it with a custom plugin. + install(createClientPlugin("RemoveAcceptCharsetHeader") { + on(Send) { request -> + request.headers.remove(HttpHeaders.AcceptCharset) + proceed(request) + } + }) + // Disable redirect following, it's handled by CalendarFetcher followRedirects = false }