From 37033b5883cb9230a2ad1127e0a0d4ab70fc5658 Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Sat, 17 Jan 2026 10:10:34 +0100 Subject: [PATCH 1/4] Filter the `Accept-Charset` header --- .../main/java/at/bitfire/icsdroid/AppHttpClient.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt b/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt index e67ce50f..bba5bc21 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,16 @@ 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 -> + // Remove the header added by Ktor by default because some servers have problems with it + request.headers.remove(HttpHeaders.AcceptCharset) + proceed(request) + } + }) + // Disable redirect following, it's handled by CalendarFetcher followRedirects = false } From 2e9098be647bd6ce4545f1b680ed011223d6cfae Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Sat, 17 Jan 2026 10:29:21 +0100 Subject: [PATCH 2/4] Remove redundant comment --- app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt b/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt index bba5bc21..ac39c456 100644 --- a/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt +++ b/app/src/main/java/at/bitfire/icsdroid/AppHttpClient.kt @@ -92,7 +92,6 @@ class AppHttpClient @AssistedInject constructor( // Ktor adds it by default, so we need to manually strip it with a custom plugin. install(createClientPlugin("RemoveAcceptCharsetHeader") { on(Send) { request -> - // Remove the header added by Ktor by default because some servers have problems with it request.headers.remove(HttpHeaders.AcceptCharset) proceed(request) } From 925c3e009986c8c86c53a23643c255b7db631d28 Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Sat, 17 Jan 2026 11:26:22 +0100 Subject: [PATCH 3/4] Adds tests --- .../java/at/bitfire/icsdroid/MockServer.kt | 9 ++- .../bitfire/icsdroid/net/TestAppHttpClient.kt | 59 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 app/src/androidTest/java/at/bitfire/icsdroid/net/TestAppHttpClient.kt 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/net/TestAppHttpClient.kt b/app/src/androidTest/java/at/bitfire/icsdroid/net/TestAppHttpClient.kt new file mode 100644 index 00000000..3e2e08c1 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/icsdroid/net/TestAppHttpClient.kt @@ -0,0 +1,59 @@ +package at.bitfire.icsdroid.net + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import at.bitfire.icsdroid.AppHttpClient +import at.bitfire.icsdroid.MockServer +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.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull + +@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)) + } +} From 51916a0c80c378733bdcd75a25cc84ac0eafd0c4 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 22 Jan 2026 11:14:37 +0100 Subject: [PATCH 4/4] Move file Signed-off-by: Arnau Mora --- .../bitfire/icsdroid/{net => }/TestAppHttpClient.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) rename app/src/androidTest/java/at/bitfire/icsdroid/{net => }/TestAppHttpClient.kt (93%) diff --git a/app/src/androidTest/java/at/bitfire/icsdroid/net/TestAppHttpClient.kt b/app/src/androidTest/java/at/bitfire/icsdroid/TestAppHttpClient.kt similarity index 93% rename from app/src/androidTest/java/at/bitfire/icsdroid/net/TestAppHttpClient.kt rename to app/src/androidTest/java/at/bitfire/icsdroid/TestAppHttpClient.kt index 3e2e08c1..0c754711 100644 --- a/app/src/androidTest/java/at/bitfire/icsdroid/net/TestAppHttpClient.kt +++ b/app/src/androidTest/java/at/bitfire/icsdroid/TestAppHttpClient.kt @@ -1,22 +1,20 @@ -package at.bitfire.icsdroid.net +package at.bitfire.icsdroid import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import at.bitfire.icsdroid.AppHttpClient -import at.bitfire.icsdroid.MockServer 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.Before -import org.junit.Test -import org.junit.runner.RunWith 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 { @@ -56,4 +54,4 @@ class TestAppHttpClient { // Assert that Accept-Charset header is not present assertFalse(headers!!.contains(HttpHeaders.AcceptCharset)) } -} +} \ No newline at end of file