-
-
Notifications
You must be signed in to change notification settings - Fork 1
Ktor example #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Ktor example #162
Changes from 13 commits
aff5694
58ac17d
f799218
d87cf8c
7fb48f5
9371411
be24aab
d9cf183
11c7bd8
fc26f8a
64c068d
12c7919
459d4dc
9613a72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -36,6 +36,11 @@ protobufPlugin = "0.9.5" # https://mvnrepository.com/artifact/com.google.prot | |||||||||||||
protobufKotlin = "4.32.0" # https://mvnrepository.com/artifact/com.google.protobuf/protobuf-kotlin | ||||||||||||||
protoc = "4.32.0" # https://mvnrepository.com/artifact/com.google.protobuf/protoc | ||||||||||||||
binaryCompatibility = "0.18.1" | ||||||||||||||
ktor = "3.2.3" | ||||||||||||||
jol = "0.17" | ||||||||||||||
retrofit-kotlinx-serialization = "1.0.0" | ||||||||||||||
protobuf = "4.31.1" | ||||||||||||||
grpc = "1.74.0" | ||||||||||||||
Comment on lines
+39
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainConsolidate versions: drop redundant aliases and avoid future drift
Apply this diff to simplify: ktor = "3.2.3"
jol = "0.17"
-retrofit-kotlinx-serialization = "1.0.0"
-protobuf = "4.31.1"
grpc = "1.74.0" Would you like me to follow through with a repo-wide update (search/replace) after switching the Retrofit converter to ensure no references to the removed alias remain? 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Show gradle/libs.versions.toml (if exists) ==="
if [ -f gradle/libs.versions.toml ]; then
sed -n '1,240p' gradle/libs.versions.toml || true
else
echo "File not found: gradle/libs.versions.toml"
fi
echo
echo "=== Search for alias usages: 'retrofit-kotlinx-serialization' ==="
rg -n --hidden --no-ignore -S "retrofit-kotlinx-serialization" || true
echo
echo "=== Search for 'retrofit' references to kotlinx serialization converter ==="
rg -n --hidden --no-ignore -S "kotlinx-serialization.*retrofit|converter.*kotlinx.*retrofit|retrofit.*kotlinx.*serialization" || true
echo
echo "=== Search for protobuf-related aliases/versions ==="
rg -n --hidden --no-ignore -S "^\s*protobuf\s*=" gradle || true
rg -n --hidden --no-ignore -S "protobufKotlin|protoc|protobuf =|\"4.31.1\"" || true
echo
echo "=== Search repository for uses of 'libs.protobuf' or similar dependency alias usage ==="
rg -n --hidden --no-ignore -S "libs\.protobuf|libs\.protobufKotlin|libs\.protoc" || true
echo
echo "=== Search for occurrences of the version string 4.31.1 anywhere ==="
rg -n --hidden --no-ignore -S "4\.31\.1" || true
echo
echo "=== Done ===" Length of output: 14641 Consolidate Protobuf versions; keep Retrofit converter alias (don't drop without verifying compatibility) Short: protobuf has a redundant version alias — remove it and point protobuf artifacts to the existing protobufKotlin/protoc alias. Do NOT remove the retrofit-kotlinx-serialization alias blindly: the kotlinx-serialization Retrofit converter is a separately versioned artifact (build.gradle.kts references libs.retrofit2.kotlinx.serialization.converter), so changing it to the Retrofit version may break builds unless you verify the converter actually publishes the matching Retrofit version. Places to update:
Suggested diffs: ktor = "3.2.3"
jol = "0.17"
retrofit-kotlinx-serialization = "1.0.0"
-protobuf = "4.31.1"
grpc = "1.74.0" -protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" }
-protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" }
+protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobufKotlin" }
+protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobufKotlin" } If you want, I can follow up with a repo-wide replace of the protobuf library refs (safe) and/or verify the retrofit-kotlinx-serialization converter versions before attempting to switch it to the Retrofit alias.
🤖 Prompt for AI Agents
|
||||||||||||||
|
||||||||||||||
[libraries] | ||||||||||||||
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutines" } | ||||||||||||||
|
@@ -66,6 +71,27 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } | |||||||||||||
sandwich-retrofit = { group = "com.github.skydoves", name = "sandwich-retrofit", version.ref = "sandwich" } | ||||||||||||||
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protoc" } | ||||||||||||||
protobuf-kotlin = { group = "com.google.protobuf", name = "protobuf-kotlin", version.ref = "protobufKotlin" } | ||||||||||||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } | ||||||||||||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } | ||||||||||||||
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } | ||||||||||||||
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } | ||||||||||||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } | ||||||||||||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } | ||||||||||||||
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } | ||||||||||||||
|
||||||||||||||
# OpenJDK JOL | ||||||||||||||
jol-core = { module = "org.openjdk.jol:jol-core", version.ref = "jol" } | ||||||||||||||
|
||||||||||||||
# Retrofit kotlinx.serialization converter | ||||||||||||||
retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit-kotlinx-serialization" } | ||||||||||||||
Comment on lines
+85
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainRetrofit 3 likely incompatible with Jake Wharton’s retrofit2-kotlinx-serialization converter. The artifact targets Retrofit 2.x (note Preferred fix: switch to Square’s official converter aligned with Retrofit 3 coordinates. -# Retrofit kotlinx.serialization converter
-retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit-kotlinx-serialization" }
+# Retrofit kotlinx.serialization converter (align version with Retrofit)
+retrofit-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } If unsure, please check the Retrofit 3 docs for the recommended converter artifact and version: 🌐 Web query:
💡 Result: Short answer
What this means and version-alignment rules
Recommended Gradle setups (examples)
Usage example (Kotlin)
Notes / gotchas
If you want, I can:
Citations:
Align with Retrofit 3’s first-party kotlinx-serialization converter • In -# Retrofit kotlinx.serialization converter
-retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit-kotlinx-serialization" }
+# Retrofit kotlinx.serialization converter (official Retrofit 3)
+converter-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } Optional: Add the Retrofit BOM to your Gradle setup to lock all Retrofit artifacts (core + converters) to the same 3.x version: // build.gradle.kts
implementation(platform("com.squareup.retrofit2:retrofit-bom:3.0.0"))
implementation("com.squareup.retrofit2:retrofit")
implementation("com.squareup.retrofit2:converter-kotlinx-serialization") 🤖 Prompt for AI Agents
|
||||||||||||||
|
||||||||||||||
# Protobuf | ||||||||||||||
protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } | ||||||||||||||
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } | ||||||||||||||
|
||||||||||||||
Comment on lines
+88
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Unify protobuf version references You’re introducing a second protobuf version alias (“protobuf”) while an existing alias (“protobufKotlin”) is already defined above. Prefer a single alias to avoid eventual skew. Apply this diff to reference the existing protobufKotlin alias: # Protobuf
-protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" }
-protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" }
+protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobufKotlin" }
+protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobufKotlin" } Since the protobuf alias (Line 42) becomes unused after this change, remove it as suggested in Lines 39–43. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||
# gRPC | ||||||||||||||
grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } | ||||||||||||||
grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } | ||||||||||||||
|
||||||||||||||
# testing libs | ||||||||||||||
junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package dev.shtanko.api | ||
|
||
internal const val BASE_URL = "https://api.github.com/" | ||
internal const val JSON_CONTENT_TYPE = "application/json" | ||
internal const val AUTH_HEADER = "Authorization" | ||
internal const val ACCEPT_HEADER = "Accept" | ||
internal const val ACCEPT_HEADER_VALUE = "application/vnd.github.v3+json" | ||
internal const val AUTH_BASIC = "Basic %s" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package dev.shtanko.api | ||
|
||
import kotlinx.serialization.json.Json | ||
|
||
internal val json = Json { ignoreUnknownKeys = true } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package dev.shtanko.api | ||
|
||
import dev.shtanko.api.model.Repo | ||
import dev.shtanko.api.model.User | ||
import io.ktor.client.HttpClient | ||
import io.ktor.client.call.body | ||
import io.ktor.client.engine.HttpClientEngine | ||
import io.ktor.client.engine.cio.CIO | ||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation | ||
import io.ktor.client.plugins.defaultRequest | ||
import io.ktor.client.plugins.logging.LogLevel | ||
import io.ktor.client.plugins.logging.Logging | ||
import io.ktor.client.request.get | ||
import io.ktor.client.request.header | ||
import io.ktor.client.request.parameter | ||
import io.ktor.http.HttpHeaders | ||
import io.ktor.serialization.kotlinx.json.json | ||
import java.util.Base64 | ||
|
||
fun createKtorClient( | ||
username: String, | ||
password: String, | ||
engine: HttpClientEngine = CIO.create(), | ||
): HttpClient { | ||
val authToken = String.format( | ||
AUTH_BASIC, | ||
Base64.getEncoder().encodeToString("$username:$password".toByteArray(Charsets.UTF_8)), | ||
) | ||
|
||
return HttpClient(engine) { | ||
install(ContentNegotiation) { | ||
json(json) | ||
} | ||
install(Logging) { | ||
level = LogLevel.INFO | ||
} | ||
defaultRequest { | ||
header(ACCEPT_HEADER, ACCEPT_HEADER_VALUE) | ||
header(HttpHeaders.Authorization, authToken) | ||
url(BASE_URL) | ||
} | ||
} | ||
} | ||
Comment on lines
+20
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainConsider security improvements for authentication and logging The current implementation has security considerations:
🏁 Script executed: #!/bin/bash
# Verify that the required constants are defined in the codebase
echo "Checking for AUTH_BASIC constant..."
rg "AUTH_BASIC\s*=" -A 2
echo "Checking for ACCEPT_HEADER constant..."
rg "ACCEPT_HEADER\s*=" -A 2
echo "Checking for ACCEPT_HEADER_VALUE constant..."
rg "ACCEPT_HEADER_VALUE\s*=" -A 2
echo "Checking for BASE_URL constant..."
rg "BASE_URL\s*=" -A 2
echo "Checking for json instance in Dependencies..."
rg "val json\s*=" src/main/kotlin/dev/shtanko/api/Dependencies.kt -A 3 Length of output: 1865 Enhance authentication security and tighten logging The required constants (
Please address these security recommendations before merging. 🤖 Prompt for AI Agents
|
||
|
||
class KtorGitHubApi(private val client: HttpClient) { | ||
|
||
suspend fun getOrgRepos(org: String): List<Repo> { | ||
return client.get("orgs/$org/repos") { | ||
parameter("per_page", 100) | ||
}.body() | ||
} | ||
Comment on lines
+47
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling and pagination support for getOrgRepos The current implementation has limitations:
Consider implementing pagination and error handling: suspend fun getOrgRepos(org: String): List<Repo> {
require(org.isNotBlank()) { "Organization name cannot be blank" }
return try {
// TODO: Implement pagination to fetch all repositories
client.get("orgs/$org/repos") {
parameter("per_page", 100)
// parameter("page", pageNumber) for pagination
}.body()
} catch (e: Exception) {
// Log error and handle appropriately
throw GitHubApiException("Failed to fetch repositories for org: $org", e)
}
} 🤖 Prompt for AI Agents
|
||
|
||
suspend fun getRepoContributors(owner: String, repo: String): List<User> { | ||
return client.get("repos/$owner/$repo/contributors") { | ||
parameter("per_page", 100) | ||
}.body() | ||
} | ||
Comment on lines
+53
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling and pagination support for getRepoContributors Similar to
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package dev.shtanko.api | ||
|
||
import dev.shtanko.api.ktor.ApiKeyInterceptor | ||
import dev.shtanko.api.ktor.HttpLogInterceptor | ||
import dev.shtanko.api.ktor.RequestResponseProcessorInterceptor | ||
import dev.shtanko.api.ktor.RetryInterceptor | ||
import io.ktor.client.HttpClient | ||
import io.ktor.client.engine.cio.CIO | ||
import io.ktor.client.plugins.HttpTimeout | ||
import io.ktor.client.plugins.logging.LogLevel | ||
import io.ktor.client.plugins.logging.Logging | ||
import io.ktor.client.request.get | ||
import io.ktor.client.statement.bodyAsText | ||
import io.ktor.client.statement.request | ||
import io.ktor.http.HttpHeaders | ||
import io.ktor.http.HttpStatusCode | ||
import java.time.Instant | ||
import kotlinx.coroutines.runBlocking | ||
|
||
@Suppress("MagicNumber", "TooGenericExceptionCaught") | ||
fun main() = runBlocking { | ||
val client = HttpClient(CIO) { | ||
// Install built-in logging | ||
install(Logging) { | ||
level = LogLevel.BODY | ||
} | ||
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Do not log secrets and full bodies in examples; sanitize headers and avoid LogLevel.BODY. With
Apply this diff to harden logging here: install(Logging) {
- level = LogLevel.BODY
+ level = LogLevel.HEADERS
+ // Optional: if you keep built-in Logging, sanitize secrets
+ logger = io.ktor.client.plugins.logging.Logger.DEFAULT
+ sanitizeHeader { h ->
+ h.equals(HttpHeaders.Authorization, ignoreCase = true) ||
+ h.equals("X-API-Key", ignoreCase = true)
+ }
}
// Install custom interceptors
install(HttpLogInterceptor) {
logRequest = true
logResponse = true
- logHeaders = true
+ // Disable header logging here to avoid leaking secrets (or add redaction in the plugin)
+ logHeaders = false
}
install(ApiKeyInterceptor) {
- apiKey = "your-api-key-here"
- headerName = "Authorization"
+ // Prefer Authorization bearer token or a custom header; do not log it
+ headerName = HttpHeaders.Authorization
+ apiKey = "Bearer ${System.getenv("API_TOKEN") ?: "your-api-key-here"}"
} Also applies to: 28-37 🤖 Prompt for AI Agents
|
||
|
||
ashtanko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Prevent requests from hanging indefinitely | ||
install(HttpTimeout) { | ||
requestTimeoutMillis = 10_000 | ||
connectTimeoutMillis = 5_000 | ||
socketTimeoutMillis = 10_000 | ||
} | ||
// Install custom interceptors | ||
install(HttpLogInterceptor) { | ||
logRequest = true | ||
logResponse = true | ||
logHeaders = true | ||
} | ||
|
||
install(ApiKeyInterceptor) { | ||
apiKey = "your-api-key-here" | ||
headerName = "Authorization" | ||
} | ||
|
||
install(RetryInterceptor) { | ||
maxRetries = 2 | ||
baseDelayMs = 500 | ||
retryOn = setOf(HttpStatusCode.InternalServerError, HttpStatusCode.ServiceUnavailable) | ||
} | ||
|
||
install(RequestResponseProcessorInterceptor) { | ||
requestProcessor = { request -> | ||
// Add timestamp to all requests | ||
request.headers.append("X-Request-Time", Instant.now().toString()) | ||
request.headers.append("X-Client-Version", "1.0.0") | ||
} | ||
|
||
responseProcessor = { response -> | ||
// Process response (logging, metrics, etc.) | ||
println("📋 Processing response from: ${response.request.url}") | ||
println("📊 Response size: ${response.headers[HttpHeaders.ContentLength] ?: "unknown"}") | ||
} | ||
} | ||
} | ||
|
||
try { | ||
// Test the interceptors | ||
val response1 = client.get("https://httpbin.org/get") | ||
println("Response 1 status: ${response1.status}") | ||
println("Response 1 body: ${response1.bodyAsText()}") | ||
|
||
// Test with a endpoint that might fail (for retry testing) | ||
val response2 = client.get("https://httpbin.org/status/500") | ||
println("Response 2 status: ${response2.status}") | ||
|
||
} catch (e: Exception) { | ||
println("Error: ${e.message}") | ||
} finally { | ||
client.close() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,34 @@ | ||||||||||||||||||||||||||||||
package dev.shtanko.api.ktor | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import io.ktor.client.HttpClient | ||||||||||||||||||||||||||||||
import io.ktor.client.plugins.HttpClientPlugin | ||||||||||||||||||||||||||||||
import io.ktor.client.request.HttpRequestPipeline | ||||||||||||||||||||||||||||||
import io.ktor.util.AttributeKey | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
class ApiKeyInterceptor private constructor( | ||||||||||||||||||||||||||||||
private val apiKey: String, | ||||||||||||||||||||||||||||||
private val headerName: String, | ||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
class Config { | ||||||||||||||||||||||||||||||
var apiKey: String = "" | ||||||||||||||||||||||||||||||
var headerName: String = "X-API-Key" | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
companion object : HttpClientPlugin<Config, ApiKeyInterceptor> { | ||||||||||||||||||||||||||||||
override val key = AttributeKey<ApiKeyInterceptor>("ApiKeyInterceptor") | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
override fun prepare(block: Config.() -> Unit): ApiKeyInterceptor { | ||||||||||||||||||||||||||||||
val config = Config().apply(block) | ||||||||||||||||||||||||||||||
require(config.apiKey.isNotEmpty()) { "API key cannot be empty" } | ||||||||||||||||||||||||||||||
return ApiKeyInterceptor(config.apiKey, config.headerName) | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
Comment on lines
+21
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Validate headerName to prevent invalid/blank headers at runtime. A blank header name will cause request failures. Guard it in prepare. Apply this diff: override fun prepare(block: Config.() -> Unit): ApiKeyInterceptor {
val config = Config().apply(block)
require(config.apiKey.isNotEmpty()) { "API key cannot be empty" }
+ require(config.headerName.isNotBlank()) { "Header name cannot be blank" }
return ApiKeyInterceptor(config.apiKey, config.headerName)
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
override fun install(plugin: ApiKeyInterceptor, scope: HttpClient) { | ||||||||||||||||||||||||||||||
scope.requestPipeline.intercept(HttpRequestPipeline.State) { | ||||||||||||||||||||||||||||||
context.headers.append(plugin.headerName, plugin.apiKey) | ||||||||||||||||||||||||||||||
println("🔑 Added API key header: ${plugin.headerName}") | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
Comment on lines
+27
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid duplicate headers; don’t println from a library plugin. Use “set” semantics to ensure only one value is present. Printing to stdout in a library is a code smell; prefer a logger or remove. Apply this diff: override fun install(plugin: ApiKeyInterceptor, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.State) {
- context.headers.append(plugin.headerName, plugin.apiKey)
- println("🔑 Added API key header: ${plugin.headerName}")
+ // Replace any existing value to avoid duplicates
+ context.headers.remove(plugin.headerName)
+ context.headers.append(plugin.headerName, plugin.apiKey)
+ // Optional: route through your logging facility instead of stdout if needed
+ // logger.debug("Added API key header: ${plugin.headerName}")
}
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid including multiple Ktor client engines unless required
Both CIO and OkHttp engines are added. This increases classpath size and can cause confusion. Prefer a single engine (CIO is already the default in createKtorClient).
Apply one of the diffs to keep a single engine:
Option A (keep CIO):
client.apply { implementation(client.core) - implementation(client.okhttp) implementation(client.content.negotiation) implementation(client.logging) implementation(client.cio) }
Option B (keep OkHttp):
client.apply { implementation(client.core) implementation(client.okhttp) implementation(client.content.negotiation) implementation(client.logging) - implementation(client.cio) }
📝 Committable suggestion
🤖 Prompt for AI Agents