From e4a0ed0a801da1bf0ff54bc09d2a42dd21d10a6d Mon Sep 17 00:00:00 2001 From: ThoughtzThruKeyz Date: Thu, 17 Sep 2026 04:33:48 -0500 Subject: [PATCH] Resolve client API paths relative to the base URL Every call in komf-client used an absolute path such as "/api/config". Ktor's defaultRequest resolves an absolute path against the host root, so when Komf is served under a subpath behind a reverse proxy, the subpath in the configured base URL is silently dropped: base URL https://host/komf -> GET https://host/api/config Use relative paths ("api/config") so requests resolve against the base URL, and normalize the base URL to end in exactly one "/". The normalization is needed too: without a trailing slash, relative resolution replaces the last path segment, so "https://host/komf" would still lose "komf". Checked against a MockEngine for base URLs "https://host", "https://host/komf" and "https://host/komf/": the latter two now request https://host/komf/api/..., and the plain host produces the same URLs as before, so setups without a subpath are unaffected. Co-Authored-By: Claude Opus 5 --- .../kotlin/snd/komf/client/KomfClientFactory.kt | 2 +- .../kotlin/snd/komf/client/KomfConfigClient.kt | 8 ++++---- .../kotlin/snd/komf/client/KomfJobClient.kt | 8 ++++---- .../snd/komf/client/KomfMediaServerClient.kt | 2 +- .../kotlin/snd/komf/client/KomfMetadataClient.kt | 4 ++-- .../snd/komf/client/KomfNotificationClient.kt | 16 ++++++++-------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfClientFactory.kt b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfClientFactory.kt index 40525986..013fd0b1 100644 --- a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfClientFactory.kt +++ b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfClientFactory.kt @@ -29,7 +29,7 @@ class KomfClientFactory private constructor(private val builder: Builder) { private val ktor: HttpClient = (builder.ktor ?: HttpClient()).config { expectSuccess = true builder.cookieStorage?.let { install(HttpCookies) { storage = it } } - defaultRequest { url(baseUrl()) } + defaultRequest { url(baseUrl().trimEnd('/') + "/") } install(ContentNegotiation) { json(json) } install(SSE) } diff --git a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfConfigClient.kt b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfConfigClient.kt index e6fc8faa..46fa2f05 100644 --- a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfConfigClient.kt +++ b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfConfigClient.kt @@ -27,11 +27,11 @@ class KomfConfigClient( ) { suspend fun getConfig(): KomfConfig { - return ktor.get("/api/config").body() + return ktor.get("api/config").body() } suspend fun updateConfig(request: KomfConfigUpdateRequest) { - ktor.patch("/api/config") { + ktor.patch("api/config") { contentType(ContentType.Application.Json) setBody(request) } @@ -40,7 +40,7 @@ class KomfConfigClient( fun updateMangaBakaDb(): Flow { return flow { runCatching { - ktor.preparePost("/api/update-manga-baka-db").execute { response -> + ktor.preparePost("api/update-manga-baka-db").execute { response -> streamProgressEvents(response.bodyAsChannel()) } }.onFailure { @@ -52,7 +52,7 @@ class KomfConfigClient( fun updateBookWalkerDb(): Flow { return flow { runCatching { - ktor.preparePost("/api/update-book-walker-db").execute { response -> + ktor.preparePost("api/update-book-walker-db").execute { response -> streamProgressEvents(response.bodyAsChannel()) } }.onFailure { diff --git a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfJobClient.kt b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfJobClient.kt index 519a0479..1d091317 100644 --- a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfJobClient.kt +++ b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfJobClient.kt @@ -32,7 +32,7 @@ class KomfJobClient( ) { suspend fun getJob(jobId: KomfMetadataJobId): KomfMetadataJob { - return ktor.get("/api/jobs/$jobId").body() + return ktor.get("api/jobs/$jobId").body() } suspend fun getJobs( @@ -40,7 +40,7 @@ class KomfJobClient( page: Int? = null, pageSize: Int? = null, ): KomfPage> { - return ktor.get("/api/jobs") { + return ktor.get("api/jobs") { status?.let { parameter("status", status.name) } page?.let { parameter("page", page) } pageSize?.let { parameter("pageSize", pageSize) } @@ -48,12 +48,12 @@ class KomfJobClient( } suspend fun getJobEvents(jobId: KomfMetadataJobId): Flow { - return ktor.sseSession("/api/jobs/${jobId.value}/events").incoming + return ktor.sseSession("api/jobs/${jobId.value}/events").incoming .map { json.toKomfEvent(it.event, it.data) } } suspend fun deleteAll() { - ktor.delete("/api/jobs/all") + ktor.delete("api/jobs/all") } diff --git a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfMediaServerClient.kt b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfMediaServerClient.kt index 703cf10a..26581b51 100644 --- a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfMediaServerClient.kt +++ b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfMediaServerClient.kt @@ -11,7 +11,7 @@ class KomfMediaServerClient( private val ktor: HttpClient, mediaServer: MediaServer ) { - private val mediaServerApiPrefix = "/api/${mediaServer.name.lowercase()}/media-server" + private val mediaServerApiPrefix = "api/${mediaServer.name.lowercase()}/media-server" suspend fun checkConnection(): KomfMediaServerConnectionResponse { return ktor.get("$mediaServerApiPrefix/connected").body() diff --git a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfMetadataClient.kt b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfMetadataClient.kt index e853dd29..cab86b77 100644 --- a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfMetadataClient.kt +++ b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfMetadataClient.kt @@ -20,10 +20,10 @@ class KomfMetadataClient( private val ktor: HttpClient, mediaServer: MediaServer ) { - private val metadataApiPrefix = "/api/${mediaServer.name.lowercase()}/metadata" + private val metadataApiPrefix = "api/${mediaServer.name.lowercase()}/metadata" suspend fun getProviders(): List { - return ktor.get("/api/metadata/providers").body() + return ktor.get("api/metadata/providers").body() } suspend fun searchSeries( diff --git a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfNotificationClient.kt b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfNotificationClient.kt index 0de75d27..41c32566 100644 --- a/komf-client/src/commonMain/kotlin/snd/komf/client/KomfNotificationClient.kt +++ b/komf-client/src/commonMain/kotlin/snd/komf/client/KomfNotificationClient.kt @@ -15,25 +15,25 @@ class KomfNotificationClient( private val ktor: HttpClient, ) { suspend fun getDiscordTemplates(): KomfDiscordTemplates { - return ktor.get("/api/notifications/discord/templates").body() + return ktor.get("api/notifications/discord/templates").body() } suspend fun updateDiscordTemplates(templates: KomfDiscordTemplates): KomfDiscordTemplates { - return ktor.post("/api/notifications/discord/templates") { + return ktor.post("api/notifications/discord/templates") { contentType(ContentType.Application.Json) setBody(templates) }.body() } suspend fun renderDiscord(request: KomfDiscordRequest): KomfDiscordRenderResult { - return ktor.post("/api/notifications/discord/render") { + return ktor.post("api/notifications/discord/render") { contentType(ContentType.Application.Json) setBody(request) }.body() } suspend fun sendDiscord(request: KomfDiscordRequest) { - ktor.post("/api/notifications/discord/send") { + ktor.post("api/notifications/discord/send") { contentType(ContentType.Application.Json) setBody(request) } @@ -41,25 +41,25 @@ class KomfNotificationClient( suspend fun getAppriseTemplates(): KomfAppriseTemplates { - return ktor.get("/api/notifications/apprise/templates").body() + return ktor.get("api/notifications/apprise/templates").body() } suspend fun updateAppriseTemplates(templates: KomfAppriseTemplates): KomfAppriseTemplates { - return ktor.post("/api/notifications/apprise/templates") { + return ktor.post("api/notifications/apprise/templates") { contentType(ContentType.Application.Json) setBody(templates) }.body() } suspend fun renderApprise(request: KomfAppriseRequest): KomfAppriseRenderResult { - return ktor.post("/api/notifications/apprise/render") { + return ktor.post("api/notifications/apprise/render") { contentType(ContentType.Application.Json) setBody(request) }.body() } suspend fun sendApprise(request: KomfAppriseRequest) { - ktor.post("/api/notifications/apprise/send") { + ktor.post("api/notifications/apprise/send") { contentType(ContentType.Application.Json) setBody(request) }