Ktor Client Version
1.6.7
Ktor Client Engine
CIO
JVM Version
1.8
Kotlin Version
1.6.10
Json Plugin
Jackson
Feedback:
The API that I'm calling returns errors with 200 HTTP Response and an error message in the response body, so I'm trying to validate the response with HttpResponseValidator but I get two kinds of errors depending on how I'm trying to get the response body.
This is the first one:
HttpClient {
install(JsonFeature)
HttpResponseValidator {
validateResponse { response ->
val responseBody = response.receive<Response>()
responseBody.sErrMsg?.let { message ->
when {
"Invalid Session ID" in message -> {
throw ResponseStatusException(
HttpStatus.UNAUTHORIZED,
message
)
}
}
}
}
}
defaultRequest {
url(URL)
headers {
contentType(ContentType.Application.Json)
}
timeout {
requestTimeoutMillis = timeoutInMillis
}
}
}
httpClient.post<Response> {
body = Request(
key = value
)
}
And I get:
io.ktor.client.call.DoubleReceiveException: Response already received: HttpClientCall[URL, 200 OK]
And this is the second way I'm trying to get the response body when doing the validation based on a similar issue (KTOR-643) but I'm facing another error:
HttpClient {
install(JsonFeature)
HttpResponseValidator {
validateResponse { response ->
val responseBody = ObjectMapper().readTree(response.readBytes())
val errorMessage = responseBody["ERROR_MESSAGE"]?.asText()
errorMessage?.let { message ->
when {
"Invalid Session ID" in message -> {
throw ResponseStatusException(
HttpStatus.UNAUTHORIZED,
message
)
}
}
}
}
}
defaultRequest {
url(URL)
headers {
contentType(ContentType.Application.Json)
}
timeout {
requestTimeoutMillis = timeoutInMillis
}
}
}
httpClient.post<Response> {
body = Request(
key = value
)
}
And I get:
com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
at [Source: (String)""; line: 1, column: 0]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59) ~[jackson-databind-2.13.1.jar:2.13.1]
So basically all I get when doing receive() after readBytes() when validating is ""
Btw, I'm using Spring Boot and the HttpClient is injected and I'm making the call in a #Service
Related
Regardless of what I do my check for when there is an exception isn't be handled. It's just being thrown in my test. I want to simulate an exception happening during the Db call and to return a 500 status code.
Given("given a db exception return Error 500") {
val expectedException = "Exception while looking up in email opt DB"
every { IDRepo.findById(any()) } throws RuntimeException(expectedException)
When("process db exception") {
val response = IDService.lookupIDValue(testEmail)
then("response server error")
response.statusCode shouldBe HttpStatus.INTERNAL_SERVER_ERROR
}
}
fun lookupIDValue(email: String): ResponseEntity<String> {
Failures.failsafeRun {
IDRepo.findById(email)
}
val IDLookupResult = IDRepo.findById(email)
return when {
IDResult == Exception() -> {
ResponseEntity("Server Error", HttpStatus.INTERNAL_SERVER_ERROR)
}
IDResult.isPresent -> {
ResponseEntity(IDResult.get().optValue.toString(), HttpStatus.OK)
}
else -> {
ResponseEntity(HttpStatus.NO_CONTENT)
}
}
}
I think, there are two issues with your code.
The first one is IDResult == Exception() as #Tenfour04 said.
The second is mockk is throwing an exception without entering code block.
IDResult == Exception() -> {
ResponseEntity("Server Er...
Without knowing about deeply your code, you should try the code below. Because you are testing the behaviour of IDRepo.findById(email) which should return a result of failure.
every { IDRepo.findById(any()) } returns Result.failure(RuntimeException(expectedException)))
Hope, it helps you.
I have an API maked with Ktor and when som field of the request failed, it returns 500 error and I want to check all request data and return, in this case, 422.
Request class:
#Serializable
data class LoginRequest (
val email: String,
val password: String
)
Routing
route("v1/auth/login") {
post {
val loginRequest = call.receive<LoginRequest>()
//LOGIN METHOD
}
}
The error that now Ktor shows is:
[eventLoopGroupProxy-4-1] ERROR Application - Unhandled: POST - /v1/auth/login
kotlinx.serialization.MissingFieldException: Field 'password' is required for type with serial name
What is the best way to ensure that the system does not fail and respond with a BadRequest?
If you wanna catch an exception in a specific place, you can use try/catch:
try {
val loginRequest = call.receive<LoginRequest>()
...
} catch (e: SerializationException) {
// serialization exceptions
call.respond(HttpStatusCode.UnprocessableEntity)
} catch (t: Throwable) {
// other exceptions
call.respond(HttpStatusCode.InternalServerError)
}
If you wanna some global try/catch, Ktor has StatusPages feature for such case: it'll catch all exceptions during calls processing.
Same as with try/catch, you can catch a specific exception, like SerializationException, or use Exception/Throwable for any other exception.
install(StatusPages) {
exception<SerializationException> { cause ->
// serialization exceptions
call.respond(HttpStatusCode.UnprocessableEntity)
}
exception<Throwable> { cause ->
// other exceptions
call.respond(HttpStatusCode.InternalServerError)
}
}
You can make fields nullable with the default null value, ignore errors when unknown properties are encountered and validate the result object manually. Here is an example:
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
#Serializable
data class LoginRequest (
val email: String? = null,
val password: String? = null
)
suspend fun main() {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
routing {
post("/") {
val request = call.receive<LoginRequest>()
if (request.email == null || request.password == null) {
call.respond(HttpStatusCode.UnprocessableEntity)
return#post
}
call.respond(HttpStatusCode.OK)
}
}
}.start()
}
post {
try {
val customer = call.receive<Customer>()
customerStorage.add(customer)
call.respondText("Customer stored correctly", status = HttpStatusCode.Created)
} catch (e: SerializationException) {
call.respondText(e.localizedMessage, status = HttpStatusCode.UnprocessableEntity)
} catch (e: Exception) {
call.respondText(e.localizedMessage, status = HttpStatusCode.InternalServerError)
}
}
I've been banging my head against the wall trying to figure out what's going wrong here for a while. I created a simple Ktor server that allows you to create a user, which should return a token to the user and store the session. Then I want an authenticated endpoint to allow the user to be deleted. However, the authenticated call loads an empty session, and can't find the user, so the user can't be deleted. Any help would be appreciated! Code here:
Application.kt
...
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
#Suppress("unused")
#kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
install(Locations) {
}
install(Sessions) {
cookie<MySession>("MY_SESSION") {
cookie.extensions["SameSite"] = "lax"
}
}
DatabaseFactory.init()
val db = MyRepository()
val jwtService = JwtService()
val hashFunction = { s: String -> hash(s) }
install(Authentication) {
jwt("jwt") { //1
verifier(jwtService.verifier) // 2
realm = "My Server"
validate { // 3
val payload = it.payload
val claim = payload.getClaim("id")
val claimString = claim.asInt()
val user = db.findUser(claimString) // 4
user
}
}
}
install(ContentNegotiation) {
gson {
}
}
routing {
users(db, jwtService, hashFunction)
}
}
UserRoute.kt
...
const val USERS = "$API_VERSION/users"
const val USER_CREATE = "$USERS/create"
const val USER_DELETE = "$USERS/delete"
#KtorExperimentalLocationsAPI
#Location(USER_CREATE)
class UserCreateRoute
#KtorExperimentalLocationsAPI
#Location(USER_DELETE)
class UserDeleteRoute
#KtorExperimentalLocationsAPI
fun Route.users(
db: Repository,
jwtService: JwtService,
hashFunction: (String) -> String
) {
post<UserCreateRoute> {
val request = call.receive<CreateUserRequest>()
val password = request.password
?: return#post call.respond(
HttpStatusCode.Unauthorized, "Missing Fields")
val email = request.email
?: return#post call.respond(
HttpStatusCode.Unauthorized, "Missing Fields")
val hash = hashFunction(password)
try {
val newUser = db.addUser(email, hash)
newUser?.userId?.let {
call.sessions.set(MySession(it))
call.respondText(
jwtService.generateToken(newUser),
status = HttpStatusCode.Created
)
}
} catch (e: Throwable) {
call.respond(HttpStatusCode.BadRequest, "Problems creating User")
}
}
authenticate("jwt") {
delete<UserDeleteRoute> {
try {
val userId = call.sessions.get<MySession>()?.userId
if (userId == null) {
call.respond(
HttpStatusCode.BadRequest, "Problem retrieving User")
return#delete
}
if (db.deleteUser(userId)) {
call.respond(HttpStatusCode.NoContent, "User deleted")
} else {
call.respond(HttpStatusCode.BadRequest, "Failed to delete user")
}
} catch (e: Exception) {
application.log.error("Failed to delete user")
call.respond(HttpStatusCode.BadRequest, "Failed to delete user")
}
}
}
}
Is there something I'm missing? The token is returned successfully, and then my delete request is routed to the right place, but the line val userId = call.sessions.get<MySession>()?.userId returns null every time.
You don't show the client code but it is just as important. Likely the problem is on the client not on server. When the clients does the delete does it send the token?
jwt would be more complicated for for basic auth after you get a session each request must include the session header:
curl -H "MY_SESSION: f152dad6e955ba53" -D - localhost:8080/api/admin/principle
I am having troubles trying to handle different errors from calling Spring webflux's web client.
Below is my current code.
return request
.bodyToMono(InputMessage::class.java)
.flatMap { inputMessage ->
client
.get()
.uri { builder ->
builder.path("/message")
.queryParam("message", inputMessage.message)
.build()
}
.retrieve()
.onStatus({t: HttpStatus -> t.is5xxServerError}, {c: ClientResponse -> Mono.error(Throwable("Internal Server Error - try again later"))})
.bodyToMono(ListOfAddresses::class.java)
}
.flatMap { s -> ServerResponse.ok().syncBody(s) }
If it errors, it is still returning the full error message from the client's call.
I tried something else, like this
return request
.bodyToMono(InputMessage::class.java)
.flatMap { inputMessage ->
client
.get()
.uri { builder ->
builder.path("/message")
.queryParam("message", inputMessage.message)
.build()
}
.retrieve()
.onStatus({t: HttpStatus -> t.is5xxServerError}, {c: ClientResponse -> Mono.error(Throwable("Internal Server Error - try again later"))})
.bodyToMono(ListOfAddresses::class.java)
}
.flatMap { s -> ServerResponse.ok().syncBody(s) }
.onErrorResume { e -> Mono.just("Error " + e.message)
.flatMap { s -> ServerResponse.ok().syncBody(s) } }
It actually works but then I want to handle different Http status codes error (different messages for each Http status code).
How can I modify my code so it will return the custom message I build?
As per WebFlux documentation, you can user the exchangeToMono() or awaitExchange { } in order to have an error handling.
Mono<Object> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else if (response.statusCode().is4xxClientError()) {
// Suppress error status code
return response.bodyToMono(ErrorContainer.class);
}
else {
// Turn to error
return response.createException().flatMap(Mono::error);
}
});
Code copied from WebFlux link above.
Take a look at 2.3. Exchange
val entity = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.awaitExchange {
if (response.statusCode() == HttpStatus.OK) {
return response.awaitBody<Person>()
}
else if (response.statusCode().is4xxClientError) {
return response.awaitBody<ErrorContainer>()
}
else {
throw response.createExceptionAndAwait()
}
}
Is there some way to define 'general' response codes that are applicable for all calls.
Eg all calls can return one of the following:
400 - Bad request
500 - Internal server error (unknown exception occurred)
503 - Service unavailable (maintenance mode)
Instead of copy-pasting the comments and attributes on every end-point it would be nice if I can define it in some central place.
Thanks #HelderSepu indeed IDocumentFilter is the solution
// Swagger config
swagger.DocumentFilter<DefaultFilter>();
internal class DefaultFilter : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var item in swaggerDoc.Paths.Values)
{
UpdateItem(item, "400", "Bad or malformed request.");
UpdateItem(item, "500", "Internal server error.");
UpdateItem(item, "503", "Service in maintenance mode.");
}
}
private static void UpdateItem(PathItem item, string key, string description)
{
TrySetValue(item.Get, key, description);
TrySetValue(item.Put, key, description);
}
private static void TrySetValue(Operation op, string key, string description)
{
if ( (op == null) || (op.Responses.ContainsKey(key)) )
{
return;
}
op.Responses.Add(key, new Response
{
Description = description,
});
}
}
For anybody using Swashbuckle 5
//in AddSwaggerGen
c.OperationFilter<GeneralExceptionOperationFilter>();
internal class GeneralExceptionOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
operation.Responses.Add("401", new OpenApiResponse() { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse() { Description = "Forbidden" });
//Example where we filter on specific HttpMethod and define the return model
var method = context.MethodInfo.GetCustomAttributes(true)
.OfType<HttpMethodAttribute>()
.Single();
if (method is HttpDeleteAttribute || method is HttpPostAttribute || method is HttpPatchAttribute || method is HttpPutAttribute)
{
operation.Responses.Add("409", new OpenApiResponse()
{
Description = "Conflict",
Content = new Dictionary<string, OpenApiMediaType>()
{
["application/json"] = new OpenApiMediaType
{
Schema = context.SchemaGenerator.GenerateSchema(typeof(string), context.SchemaRepository)
}
}
});
}
}
}