Skip to content

Commit

Permalink
fix response body consumed on error, update dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
weickmanna committed Dec 1, 2023
1 parent b6d638c commit 82bdfb7
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 44 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

[![Build Status](https://github.com/linked-planet/ktor-plugins/actions/workflows/default.yml/badge.svg)](https://github.com/linked-planet/ktor-plugins/actions/workflows/default.yml)
[![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0)
[![ktor Version](https://img.shields.io/badge/ktor-2.3.6-blue)](https://ktor.io/)

Contains several useful plugins extending the [ktor][ktor] framework.

## ktor-server

### ktor-server-onelogin-saml

Integrates [ktor][ktor] with onelogin's [java-saml][java-saml] library.
Integrates ktor-server with onelogin's [java-saml][java-saml] library.

## ktor-client

### ktor-client-awesome-logging

Awesome logging experience for [ktor][ktor] HttpClient.
Awesome logging experience for ktor-client.


[ktor]: https://ktor.io
Expand Down
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
plugins {
kotlin("jvm") version "1.9.20" apply (false)
kotlin("jvm") version "1.9.21" apply (false)

// derive gradle version from git tag
id("pl.allegro.tech.build.axion-release") version "1.15.5"
id("pl.allegro.tech.build.axion-release") version "1.16.0"

// publishing
id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
Expand All @@ -14,7 +14,7 @@ plugins {
id("se.ascp.gradle.gradle-versions-filter") version "0.1.16"
}

ext.set("kotlinVersion", "1.9.20")
ext.set("kotlinVersion", "1.9.21")
ext.set("ktorVersion", "2.3.6")

val libVersion: String = scmVersion.version
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
1 change: 1 addition & 0 deletions ktor-client-awesome-logging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![Maven Central](https://img.shields.io/maven-central/v/com.linked-planet/ktor-client-awesome-logging.svg?label=central)](https://central.sonatype.com/search?q=ktor-client-awesome-logging&namespace=com.linked-planet)
[![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0)
[![ktor Version](https://img.shields.io/badge/ktor-2.3.6-blue)](https://ktor.io/)

Awesome logging experience for [ktor][ktor] HttpClient.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,16 @@ package com.linkedplanet.ktor.client.logging

import io.ktor.client.*
import io.ktor.client.plugins.api.*
import io.ktor.client.plugins.observer.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.client.utils.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.slf4j.MDCContext
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.slf4j.event.Level
import java.nio.charset.Charset
import java.util.*
Expand Down Expand Up @@ -70,10 +67,16 @@ interface Logger {

private val traceIdKey = AttributeKey<String>("traceId")

@OptIn(InternalAPI::class)
val AwesomeClientLogging = createClientPlugin("AwesomeClientLogging", ::AwesomeClientLoggingConfig) {
val config = pluginConfig

/*
* The following line is needed so we can read the response body multiple times.
* Without this treatment, we are "consuming" the response body, making it unavailable to the application.
* -> Should not be needed anymore starting from ktor 3.0.0 (SaveBodyPlugin should provide this functionality).
*/
client.receivePipeline.intercept(HttpReceivePipeline.State) { response -> interceptReceive(config, response) }

on(SendingRequest) { request, content ->
val body =
if (!config.requestConfig.logBody) null
Expand All @@ -86,20 +89,21 @@ val AwesomeClientLogging = createClientPlugin("AwesomeClientLogging", ::AwesomeC
}
logRequest(config, request, body)
}
}

onResponse { response ->
val body =
if (response.status.isError() && config.responseConfig.logBodyOnError) {
val wrapped = response.call.wrapWithContent(response.content)
wrapped.response.contentType()?.let { contentType ->
val charset = contentType.charset() ?: Charsets.UTF_8
wrapped.response.content.tryReadText(charset) ?: "[response body unavailable]"
}
} else {
null
}
logResponse(config, response, body)
}
private suspend fun PipelineContext<HttpResponse, Unit>.interceptReceive(
config: AwesomeClientLoggingConfig,
response: HttpResponse,
) {
val responseData = CachedHttpResponseData.create(response)

val body =
if (response.status.isError() && config.responseConfig.logBodyOnError)
responseData.body.ifEmpty { "[response body unavailable]" }
else null
logResponse(config, response, body)

proceedWith(CachedHttpResponse(response.call, responseData, response.coroutineContext))
}

private suspend fun logRequest(config: AwesomeClientLoggingConfig, request: HttpRequestBuilder, body: String? = null) {
Expand Down Expand Up @@ -186,23 +190,3 @@ class AwesomeClientLoggingConfig {
)

}

suspend fun <T> withMdc(vararg infos: Pair<String, Any?>, func: suspend () -> T): T =
withMdc(MDC.getCopyOfContextMap() ?: emptyMap(), infos.toMap(), func)

private suspend fun <T> withMdc(oldState: Map<String, String>, newState: Map<String, Any?>, func: suspend () -> T): T {
newState.entries.forEach { (key, value) ->
MDC.put(key, value.toString())
}
return try {
withContext(MDCContext()) {
func()
}
} finally {
// the MDC context does not reliably capture the old context before switching into the new context
// this leads to values getting lost when restoring the old context, so we take care of it ourselves
oldState.entries.forEach { (key, value) ->
MDC.put(key, value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.linkedplanet.ktor.client.logging

import io.ktor.client.call.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
import io.ktor.util.date.*
import io.ktor.utils.io.*
import kotlin.coroutines.CoroutineContext

class CachedHttpResponse(
override val call: HttpClientCall,
response: CachedHttpResponseData,
override val coroutineContext: CoroutineContext
) : HttpResponse() {
@InternalAPI
override val content: ByteReadChannel = ByteReadChannel(response.body)
override val headers: Headers = response.headers.headers.filterNot { it.name == "content-encoding" }.toHeaders()
override val requestTime: GMTDate = GMTDate(response.headers.timestamp)
override val responseTime: GMTDate = GMTDate()
override val status: HttpStatusCode = HttpStatusCode.fromValue(response.headers.statusCode)
override val version: HttpProtocolVersion = HttpProtocolVersion.HTTP_1_1
}

private fun List<CachedHttpHeaderValue>.toHeaders(): Headers = HeadersBuilder().apply {
forEach { header -> append(header.name, header.value) }
}.build()

data class CachedHttpResponseData(val headers: CachedHttpHeaders, val body: String) {
companion object {
suspend fun create(response: HttpResponse): CachedHttpResponseData {
val body = try {
response.bodyAsText()
} catch (e: Exception) {
""
}
return CachedHttpResponseData(CachedHttpHeaders(response), body)
}
}
}

data class CachedHttpHeaders(val statusCode: Int, val timestamp: Long, val headers: List<CachedHttpHeaderValue>) {
constructor(response: HttpResponse) : this(
response.status.value, response.responseTime.timestamp,
response.headers.entries().flatMap { (name, values) ->
values.map { CachedHttpHeaderValue(name, it) }
}
)
}

data class CachedHttpHeaderValue(val name: String, val value: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.linkedplanet.ktor.client.logging

import kotlinx.coroutines.slf4j.MDCContext
import kotlinx.coroutines.withContext
import org.slf4j.MDC

suspend fun <T> withMdc(vararg infos: Pair<String, Any?>, func: suspend () -> T): T =
withMdc(MDC.getCopyOfContextMap() ?: emptyMap(), infos.toMap(), func)

private suspend fun <T> withMdc(oldState: Map<String, String>, newState: Map<String, Any?>, func: suspend () -> T): T {
newState.entries.forEach { (key, value) ->
MDC.put(key, value.toString())
}
return try {
withContext(MDCContext()) {
func()
}
} finally {
// the MDC context does not reliably capture the old context before switching into the new context
// this leads to values getting lost when restoring the old context, so we take care of it ourselves
oldState.entries.forEach { (key, value) ->
MDC.put(key, value)
}
}
}
1 change: 1 addition & 0 deletions ktor-server-onelogin-saml/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![Maven Central](https://img.shields.io/maven-central/v/com.linked-planet/ktor-client-awesome-logging.svg?label=central)](https://central.sonatype.com/search?q=ktor-server-onelogin-saml&namespace=com.linked-planet)
[![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0)
[![ktor Version](https://img.shields.io/badge/ktor-2.3.6-blue)](https://ktor.io/)

Integrates [ktor](ktor.io) with onelogin's
[java-saml](https://github.com/onelogin/java-saml) library.
Expand Down

0 comments on commit 82bdfb7

Please sign in to comment.