How can the status code for a response provided by a #ExceptionHandler be set when the #Controller returns a reactive type (Mono)? - kotlin

How can the status code for a response provided by a #ExceptionHandler be set when the #Controller returns a reactive type (Mono)?
It seems that it is not possible via returning a ResponseEntity or annotating the #ExceptionHandler method with #ResponseStatus.
A fairly minimal test showing the issue (note that the response body and content type are correctly verified while the status code is OK when it should be INTERNAL_SERVER_ERROR):
class SpringWebMvcWithReactiveResponseTypeExceptionHandlerCheckTest {
#RestController
#RequestMapping("/error-check", produces = ["text/plain;charset=UTF-8"])
class ExceptionHandlerCheckController {
#GetMapping("errorMono")
fun getErrorMono(): Mono<String> {
return Mono.error(Exception())
}
}
#ControllerAdvice
class ErrorHandler : ResponseEntityExceptionHandler() {
#ExceptionHandler(Exception::class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun handleException(ex: Exception): ResponseEntity<*> = ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(mapOf("key" to "value"))
}
val mockMvc = MockMvcBuilders
.standaloneSetup(ExceptionHandlerCheckController())
.setControllerAdvice(ErrorHandler())
.build()
#Test
fun `getErrorMono returns HTTP Status OK instead of the one set by an ExceptionHandler`() {
mockMvc.get("/error-check/errorMono")
// .andExpect { status { isInternalServerError() } }
.andExpect { status { isOk() } }
.asyncDispatch()
.andExpect {
content {
contentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE)
json("""
{
"key": "value"
}
""",strict = true
)
}
}
}
}
(build.gradle.kts excerpt showing relevant dependencies):
plugins: {
id("org.springframework.boot") version "2.4.5"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}

The issue is that .andExpect { status ... has to be called after .asyncDispatch() (as was done with content type and body). It seems that the http status is updated. Possibly this is actually part of http standard for async requests, but I suspect this is a bug of the underlying MockHttpServletResponse.

Related

Mocking internal function call in Kotlin

I am a complete beginner in terms of Kotlin and I am finding some issues while trying to test out a Ktor based application.
I have a file in my endpoints package localized at org.example.endpoints.hello
this file contains a fun Application.hello that implements an endpoint for my application.
This endpoint acts as a wrapper for another API, so inside that same file I have a
fun callOtherAPI(): ResponseContainer {
// networking stuff
return ResponseContainer(message: "some stuff")
}
This function gets called inside the Application's function routing implementation as such:
routing {
get("/hello") {
call.respond(callOtherAPI())
}
}
Now to the issue:
My test currently looks like this:
#Test
fun testHello() = testApplication {
application {
hello()
}
mockkStatic(::callOtherAPI)
every { callOtherAPI() } returns ResponseContainer("hello")
print(callOtherAPI()) // This actually returns the mocked response, which is what I want
client.get("/hello").apply {
val expected = ResponseContainer("hello")
val response = jacksonObjectMapper().readValue<ResponseContainer>(bodyAsText())
assertEquals(HttpStatusCode.OK, status)
assertEquals(expected.message, response.message) // This assert fails because the internal call to callOtherAPI() is not being mocked.
}
}
So the problem that I am facing is that while the mocked function is being mocked within the context of the test, it is not being mocked when called internally by the routing implementation.
Can someone point me to good documentation to figure this out, I've been at it for the past two hours to no avail :/
Thanks!
You can declare a parameter for the callOtherAPI function in the hello method. For the production and testing environment you will pass different functions in this case. Here is your code rewritten:
#Test
fun testHello() = testApplication {
application {
// hello(::callOtherAPI) this call will be for the production environment
hello { ResponseContainer("hello") }
}
client.get("/hello").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals("{\"message\":\"hello\"}", bodyAsText())
}
}
data class ResponseContainer(val message: String)
fun Application.hello(callOtherAPI: () -> ResponseContainer) {
install(ContentNegotiation) {
jackson()
}
routing {
get("/hello") {
call.respond(callOtherAPI())
}
}
}
fun callOtherAPI(): ResponseContainer {
// networking stuff
return ResponseContainer("some stuff")
}

Is there any way to drop request inside plugin

Now I'm developing server application with ktor 2(2.0.0-eap-256).
What I want to do is, according to header or other information, Reject or set adequate http status to response and do not let request go into service logic.
Below is What I tried.
val testPlugin = createApplication("testPlugin") {
onCall {
if (call.request.headers["auth"] == null) {
call.respond(HttpStatusCode.BadRequest)
return#onCall
}
}
}
fun Application.testRouting() {
routing {
get("/") { call.respond("hello") }
}
}
fun Application.applyPlugin() {
install(testPlugin)
}
But request goes into service logic defined by routing(with response which has HttpStatusCode.BadRequest). Is there any idea?
And also, I want to ask my understand about onCall/onCallReceive/onCallRespond is right
onCall is invoked first, when request come.
then, onCallReceive is invoked to handle request data such as file, body, etc
after all service logic, onCallRespond is invoked.
Edit
About the last question, it is solved. onCallReceive is called when I invoke call.receive() to get request content
Edit
Add routing code
Edit
So, I edit plugin like this.
val testPlugin = createApplication(
name = "testPlugin",
createConfiguration = { TestPluginConfig() }
) {
pluginConfig.apply {
pipeline!!.intercept(ApplicationCallPipeline.Plugins){
if (call.request.headers["auth"] == null) {
call.respond(HttpStatusCode.BadRequest)
finish()
}
}
}
}
data class TestPluginConfig(
var pipeline: Application? = null // io.ktor.sever.Application
)
fun Application.testRouting() {
routing {
get("/") { call.respond("hello") }
}
}
fun Application.applyPlugin() {
val pipeline = this // io.ktor.sever.Application
install(testPlugin) { pipeline = pipeline }
}
It works just as I wanted
very thanks to Aleksei Tirman
According to Rustam Siniukov on the kotlin slack here it's enough to use call.respond in the plugin.
My tests confirmed this.

Ktor Server/Application request/response body logging

Is there any way to log the request and response body from the ktor server communication?
The buildin CallLogging feature only logs the metadata of a call. I tried writing my own logging feature like in this example: https://github.com/Koriit/ktor-logging/blob/master/src/main/kotlin/korrit/kotlin/ktor/features/logging/Logging.kt
class Logging(private val logger: Logger) {
class Configuration {
var logger: Logger = LoggerFactory.getLogger(Logging::class.java)
}
private suspend fun logRequest(call: ApplicationCall) {
logger.info(StringBuilder().apply {
appendLine("Received request:")
val requestURI = call.request.path()
appendLine(call.request.origin.run { "${method.value} $scheme://$host:$port$requestURI $version" })
call.request.headers.forEach { header, values ->
appendLine("$header: ${values.firstOrNull()}")
}
try {
appendLine()
appendLine(String(call.receive<ByteArray>()))
} catch (e: RequestAlreadyConsumedException) {
logger.error("Logging payloads requires DoubleReceive feature to be installed with receiveEntireContent=true", e)
}
}.toString())
}
private suspend fun logResponse(call: ApplicationCall, subject: Any) {
logger.info(StringBuilder().apply {
appendLine("Sent response:")
appendLine("${call.request.httpVersion} ${call.response.status()}")
call.response.headers.allValues().forEach { header, values ->
appendLine("$header: ${values.firstOrNull()}")
}
when (subject) {
is TextContent -> appendLine(subject.text)
is OutputStreamContent -> appendLine() // ToDo: How to get response body??
else -> appendLine("unknown body type")
}
}.toString())
}
/**
* Feature installation.
*/
fun install(pipeline: Application) {
pipeline.intercept(ApplicationCallPipeline.Monitoring) {
logRequest(call)
proceedWith(subject)
}
pipeline.sendPipeline.addPhase(responseLoggingPhase)
pipeline.sendPipeline.intercept(responseLoggingPhase) {
logResponse(call, subject)
}
}
companion object Feature : ApplicationFeature<Application, Configuration, Logging> {
override val key = AttributeKey<Logging>("Logging Feature")
val responseLoggingPhase = PipelinePhase("ResponseLogging")
override fun install(pipeline: Application, configure: Configuration.() -> Unit): Logging {
val configuration = Configuration().apply(configure)
return Logging(configuration.logger).apply { install(pipeline) }
}
}
}
It works fine for logging the request body using the DoubleReceive plugin. And if the response is plain text i can log the response as the subject in the sendPipeline interception will be of type TextContent or like in the example ByteArrayContent.
But in my case i am responding a data class instance with Jackson ContentNegotiation. In this case the subject is of type OutputStreamContent and i see no options to geht the serialized body from it.
Any idea how to log the serialized response json in my logging feature? Or maybe there is another option using the ktor server? I mean i could serialize my object manually and respond plain text, but thats an ugly way to do it.
I'm not shure about if this is the best way to do it, but here it is:
public fun ApplicationResponse.toLogString(subject: Any): String = when(subject) {
is TextContent -> subject.text
is OutputStreamContent -> {
val channel = ByteChannel(true)
runBlocking {
(subject as OutputStreamContent).writeTo(channel)
val buffer = StringBuilder()
while (!channel.isClosedForRead) {
channel.readUTF8LineTo(buffer)
}
buffer.toString()
}
}
else -> String()
}

Ktor is throwing a "Lost in ambiguity tie" error on GET requests

I have a project that was running on Kotlin 1.4.0 and Ktor 1.3.2, so I decided to try and upgrade my versions to Kotlin 1.4.30 and Ktor 1.5.1 (since I was also upgrading to Java 11 due to adoptopenJDK8 not having JavaFx).
When I finally fixed import issues and whatnot and ran my webserver, the first page opens correctly (it's mapped to route "/"), but the other GET requests throw a "Lost in ambiguity tie" error on the RoutingResolve class. Apparently when comparing the quality of registered selectors, the "/ping" one has the same value as "//", hence throwing the error. I have validated this behaviour Ktor 1.5.1 and 1.5.0. Versions 1.4.x throw coroutine errors.
My server configurations are declared as such:
fun serverConfiguration(controller: Controller): NettyApplicationEngine {
SocketRoutes.controller = controller
WebUIRoutes.controller = controller
return embeddedServer(Netty, 9090) {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
dateFormat = DateFormat.getDateInstance()
deactivateDefaultTyping()
}
}
install(FreeMarker) {
templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
defaultEncoding = Charsets.UTF_8.toString()
}
install(CallLogging)
install(Sessions) {
cookie<UserPreferences>("preferences")
}
getRoutes(this)
}
}
My Routes.kt
fun getRoutes(pipeline: Application): Routing {
return Routing.install(pipeline) {
trace { application.log.trace(it.buildText()) }
static("/") {
resources("web-static")
}
home()
getTableBody()
ping()
}
}
And these routes themselves are on WebUIRoutes.kt
object WebUIRoutes {
lateinit var controller: Controller
fun Route.home() {
get("/") {
val session = call.sessions.get<UserPreferences>() ?: UserPreferences()
call.sessions.set(session)
val nrPages = calculateNrOfPages(session.nrElements)
call.response.cookies["nrElements"]?.copy(value = "$nrPages")
call.respondTemplate(
"showCodes.ftl", mapOf(
"total" to nrPages, "nrElements" to session.nrElements,
"validateOnInput" to session.validateOnInput
)
)
}
}
(...)
fun Route.ping() {
get("/ping") {
if (controller.ping())
call.respond(HttpStatusCode.OK)
else
call.respond(HttpStatusCode.NotFound)
}
}
}
So I have no idea what the hell is going on and I've found nothing online about migrating to newer versions of Ktor and related issues...
The problem is that the resources function adds a route with a wildcard under the / route and therefore the routing resolves it instead of the /ping route. You can just move the static function call to the end of Routing to make it work as you expect.

How can I override logRequest/logResponse to log custom message in Ktor client logging?

Currently, the ktor client logging implementation is as below, and it works as intended but not what I wanted to have.
public class Logging(
public val logger: Logger,
public var level: LogLevel,
public var filters: List<(HttpRequestBuilder) -> Boolean> = emptyList()
)
....
private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
if (level.info) {
logger.log("REQUEST: ${Url(request.url)}")
logger.log("METHOD: ${request.method}")
}
val content = request.body as OutgoingContent
if (level.headers) {
logger.log("COMMON HEADERS")
logHeaders(request.headers.entries())
logger.log("CONTENT HEADERS")
logHeaders(content.headers.entries())
}
return if (level.body) {
logRequestBody(content)
} else null
}
Above creates a nightmare while looking at the logs because it's logging in each line. Since I'm a beginner in Kotlin and Ktor, I'd love to know the way to change the behaviour of this. Since in Kotlin, all classes are final unless opened specifically, I don't know how to approach on modifying the logRequest function behaviour. What I ideally wanted to achieve is something like below for an example.
....
private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
...
if (level.body) {
val content = request.body as OutgoingContent
return logger.log(value("url", Url(request.url)),
value("method", request.method),
value("body", content))
}
Any help would be appreciative
No way to actually override a private method in a non-open class, but if you just want your logging to work differently, you're better off with a custom interceptor of the same stage in the pipeline:
val client = HttpClient(CIO) {
install("RequestLogging") {
sendPipeline.intercept(HttpSendPipeline.Monitoring) {
logger.info(
"Request: {} {} {} {}",
context.method,
Url(context.url),
context.headers.entries(),
context.body
)
}
}
}
runBlocking {
client.get<String>("https://google.com")
}
This will produce the logging you want. Of course, to properly log POST you will need to do some extra work.
Maybe this will be useful for someone:
HttpClient() {
install("RequestLogging") {
responsePipeline.intercept(HttpResponsePipeline.After) {
val request = context.request
val response = context.response
kermit.d(tag = "Network") {
"${request.method} ${request.url} ${response.status}"
}
GlobalScope.launch(Dispatchers.Unconfined) {
val responseBody =
response.content.tryReadText(response.contentType()?.charset() ?: Charsets.UTF_8)
?: "[response body omitted]"
kermit.d(tag = "Network") {
"${request.method} ${request.url} ${response.status}\nBODY START" +
"\n$responseBody" +
"\nBODY END"
}
}
}
}
}
You also need to add a method from the Ktor Logger.kt class to your calss with HttpClient:
internal suspend inline fun ByteReadChannel.tryReadText(charset: Charset): String? = try {
readRemaining().readText(charset = charset)
} catch (cause: Throwable) {
null
}