-
-
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 1 commit
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.31.1" # https://mvnrepository.com/artifact/com.google.protobuf/protobuf-kotlin | ||||||||||||||
protoc = "4.31.1" # 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.73.0" | ||||||||||||||
|
||||||||||||||
[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,50 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
package dev.shtanko.api | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import io.ktor.client.HttpClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import io.ktor.client.engine.cio.CIO | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import io.ktor.client.plugins.HttpSend | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import io.ktor.client.plugins.logging.LogLevel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import io.ktor.client.plugins.logging.Logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import io.ktor.client.plugins.plugin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import io.ktor.client.request.HttpRequestPipeline | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import io.ktor.client.request.HttpSendPipeline | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import io.ktor.client.request.get | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import kotlinx.coroutines.runBlocking | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Suppress("MagicNumber") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fun main() = runBlocking { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
val client = HttpClient(CIO) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
install(Logging) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
level = LogLevel.ALL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client.sendPipeline.intercept(HttpSendPipeline.State) { request -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
println("Intercepted request to: $request") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// For example, add a custom header | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
context.headers.append("X-Custom-Header", "MyValue") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
proceed() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client.requestPipeline.intercept(HttpRequestPipeline.Before) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
println("Sending request to: ${context.url} ${context.method}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
proceed() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client.plugin(HttpSend).intercept { request -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
println("Request URL: ${request.url}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
println("Request method: ${request.method}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
println("Request headers: ${request.headers}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
val originalCall = execute(request) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (originalCall.response.status.value !in 100..399) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
execute(request) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
originalCall | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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. Do not log sensitive headers (Authorization/PII) Logging the entire header set can leak credentials. Mask or drop sensitive headers before printing. - println("Request headers: ${request.headers}")
+ val safeHeaders = request.headers.entries().associate { (k, v) ->
+ if (k.equals("Authorization", ignoreCase = true)) k to listOf("***") else k to v
+ }
+ println("Request headers: $safeHeaders") 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
val response = client.get("https://httpbin.org/get") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
println("Response status: ${response.status}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client.close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 Close the client reliably Ensure the client is closed on exceptions as well. - val response = client.get("https://httpbin.org/get")
- println("Response status: ${response.status}")
-
- client.close()
+ try {
+ val response = client.get("https://httpbin.org/get")
+ println("Response status: ${response.status}")
+ } finally {
+ client.close()
+ } 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package dev.shtanko.api.model | ||
|
||
import kotlinx.serialization.Serializable | ||
|
||
@Serializable | ||
data class Repo( | ||
val id: Long, | ||
val name: String, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package dev.shtanko.api.model | ||
|
||
import kotlinx.serialization.Serializable | ||
|
||
@Serializable | ||
data class RequestData( | ||
val username: String, | ||
val password: String, | ||
val org: String, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package dev.shtanko.api.model | ||
|
||
import kotlinx.serialization.Serializable | ||
|
||
@Serializable | ||
data class User( | ||
val login: String, | ||
val contributions: Int, | ||
) |
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