Validate Request in Ktor - validation

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)
}
}

Related

Exception being thrown and returning status code instead

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.

Ktor Client error when validating 200 responses with HttpResponseValidator

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

Ktor session not being found

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

how to use socket IO in kotlin?

I want to initialize socket IO in my kotlin app.
my problem is here :
private var mSocket: Socket? = null
{
try {
mSocket = IO.socket("http://chat.socket.io")
} catch (URISyntaxException e) {
}
}
import com.github.nkzawa.socketio.client.IO
cant recognize
I searched for this some more and found this solution:
You connect your ws like this:
val opts = IO.Options()
opts.path = "/path/to/ws"
opts.transports = arrayOf(WebSocket.NAME) // Set the transfer to 'websocket' instead of 'polling'
val webSocket = IO.socket("http(s)://your.ip.here", opts)
webSocket.connect()
.on(Socket.EVENT_CONNECT) {
// Do your stuff here
}
.on("foo") { parameters -> // do something on recieving a 'foo' event
// 'parameters' is an Array of all parameters you sent
// Do your stuff here
}
If you want to emit an event, you'll call:
webSocket.emit("foo", "bar") // Emits a 'foo' event with 'bar' as a parameter
You will need to use
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
so be sure to add the corresponding libraries to your build.gradle
dependencies {
...
implementation 'com.github.nkzawa:socket.io-client:0.6.0'
}
first import this
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
and then initialize this one
val socket = IO.socket("http://localhost:4000/")
socket.on(Socket.EVENT_CONNECT, Emitter.Listener {
socket.emit("messages", "hi")
});
socket.connect()
It's a static block in java But we can't wirte same as in Kotlin.
We can use its like a companion object.
companion object{
private var mSocket: Socket?=null
init {
try {
mSocket = IO.socket(Constants.Chat_URl)
}
catch (e: Exception){
throw RuntimeException(e)
}
}
}
In Kotlin you can make a Socket Client like the following. All the Exceptions are handled here too.
fun pingYourTCPServerWith(message: String): String{
try {
val socket = Socket("<YOUR IP ADDRESS>", <YOUR PORT HERE>)
socket.use {
var responseString : String? = null
it.getOutputStream().write(message.toByteArray())
val bufferReader = BufferedReader(InputStreamReader(it.inputStream))
while (true) {
val line = bufferReader.readLine() ?: break
responseString += line
if (line == "exit") break
}
println("Received: $responseString")
bufferReader.close()
it.close()
return responseString!!
}
}catch (he: UnknownHostException){
val exceptionString = "An exception occurred:\n ${he.printStackTrace()}"
return exceptionString
}catch (ioe: IOException){
val exceptionString = "An exception occurred:\n ${ioe.printStackTrace()}"
return exceptionString
} catch (ce: ConnectException){
val exceptionString = "An exception occurred:\n ${ce.printStackTrace()}"
return exceptionString
}catch (se: SocketException){
val exceptionString = "An exception occurred:\n ${se.printStackTrace()}"
return exceptionString
}
}
The right syntax is below for anyone who is interested in the future
private lateinit var mSocket:Socket
fun socket(){
try {
mSocket=IO.socket("http://host:port")
}
catch(e: URISyntaxException){
println("Exception"+e)
}
}

Trapping errors in Aurelias HTTP client

Hi All (Especially the Aurelia core team hanging about round here)
I have an aurelia app using the "aurelia-http-client" to make requests to my back end API.
My back end API is a C# based service running on Nancy.
In my front end Iv'e abstracted the http client out to my own network lib as follows:
import { inject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
import { HttpClient } from 'aurelia-http-client';
import environment from './environment';
#inject(HttpClient, Router)
export default class httpservice {
private http: HttpClient = null;
private router: Router = null;
private authService: any = null;
private authToken: string = "";
constructor(HttpClient, Router) {
this.http = HttpClient;
this.router = Router;
HttpClient.configure(http => {
http.withBaseUrl(environment.servicebase);
});
}
public setAuthService(authService: any) {
this.authService = authService;
}
public get(url: string, authObject?: any): any {
let myAuth = this.authService ? this.authService : authObject;
let myToken = "";
if (myAuth) {
myToken = myAuth.getAuthToken();
}
let self = this;
let client = this.http
.createRequest(url)
.asGet()
.withHeader("AuthenticationToken", myToken)
.withInterceptor({
responseError(responseError) {
console.log(responseError);
if (responseError.statusCode === 401) {
if (myAuth) {
myAuth.destroySession();
}
}
if (responseError.statusCode === 404) {
self.router.navigateToRoute("missing");
}
return responseError;
}
});
return client;
}
public post(url: string, postData: any, authObject?: any): any {
let myAuth = this.authService ? this.authService : authObject;
let myToken = "";
if (myAuth) {
myToken = myAuth.getAuthToken();
}
let self = this;
let client = this.http
.createRequest(url)
.asPost().withContent(postData)
.withHeader("AuthenticationToken", myToken)
.withInterceptor({
responseError(responseError) {
console.log(responseError);
if (responseError.statusCode === 401) {
if (myAuth) {
myAuth.destroySession();
}
}
if (responseError.statusCode === 404) {
self.router.navigateToRoute("missing");
}
return responseError;
}
});
return client;
}
}
and I then use this in my other modules/classes as follows:
import { Aurelia, inject } from 'aurelia-framework';
import HttpService from './httpservice';
import environment from './environment';
import { EventAggregator } from 'aurelia-event-aggregator';
#inject(EventAggregator, Aurelia, HttpService)
export default class Authservice {
public http: HttpService = null;
public app: Aurelia = null;
public ea: EventAggregator = null;
public authToken: any = null;
private loginUrl: string = "";
private logoutUrl: string = "";
private checkUrl: string = "";
constructor(eventAggregator, aurelia, httpService) {
this.http = httpService;
this.app = aurelia;
this.ea = eventAggregator;
this.loginUrl = "/login";
}
public getAuthToken() {
if (!sessionStorage[environment.tokenname] ||
(sessionStorage[environment.tokenname] == null)) {
return null;
}
return sessionStorage[environment.tokenname];
}
public login(loginName, password) {
let postData = {
loginName: loginName,
password: password
};
let client = this.http.post(this.loginUrl, postData);
client.send()
.then((response) => response.content)
.then((data) => {
if (data.error) {
this.ea.publish("loginMessage", { message: data.errorMessage });
return;
}
if (data.authenticationFailed) {
this.ea.publish("loginMessage", { message: "Invalid user name and/or password supplied." });
return;
}
if (data.accountSuspended) {
this.ea.publish("loginMessage", { message: "Your account has been suspended, please contact support." });
return;
}
sessionStorage[environment.tokenname] = data.token;
sessionStorage["displayedLoginName"] = data.displayName;
location.assign('#/');
this.app.setRoot('app');
})
.catch(() =>
{
debugger;
alert("Something bad happened trying to connect to server.");
});
}
public isAuthenticated() {
// TODO: hook this up to check auth token validity via rest call???
let token = this.getAuthToken();
return token !== null;
}
}
enum LoginStates {
LoginValid = 0,
BadUserNameOrPassword,
AccountSuspended
}
Please note I've stripped some of the code out of the auth library to reduce confusion
In general ALL of this works well. The interceptors get triggered when 401s and 404s occur, and if I add a 500 that get's handled too, so where all good there.
The problem I have is handling communication failures.
As you can see in the login routine, I have a catch following the then.
I expected that if the server couldn't be reached or some other base communications failure occurred, that this catch would trigger rather than the "then" and thus allow me to handle the error, but instead it does not.
What I get instead is this in the console:
Worse still, my login routine doesn't abort, it actually succeeds and allows the logged in page to be shown.
It seems that while the library is making the OPTIONS call (Which is when this error occurs) none of my user code is taken into account.
The OPTIONS call is required for successful pre-flight/ajax requests, so stopping this happening is not an option, and I feel that if the OPTIONS call did not abort, but made it to the POST call,t hat my error handling would then be taken into consideration.
It seems silly to be not able to trap errors like this, especially in today's mobile world where a device may be out of coverage or temporarily offline.
If anyone has any thoughts on how this can be solved, I'd love to hear them.
Update 1
My problem seems to be related to this one:
aurelia-fetch-client - a promise was rejected with a non-error: [object Response]
However, I'm not using "useStandardConfiguration()" which is apparently the cause for that case. I'm also not using the fetch client, however I do note that the API in both clients is practically the same, so I wonder if the underlying code is also similar.
Ok.... so, after a long hard afternoon of head scratching and hair pulling, it turns out, the whole thing is actually linked to a reported issue with the "BlueBird promises library" which is what aurelia uses to manage it's promises.
The link to the issue with BlueBird can be found here:
https://github.com/petkaantonov/bluebird/issues/990
It's not specifically an issue according to the BB dev's but to many folks encountering it, it sure looks like one.
The bottom line is that the library is not designed to throw the errors generated directly by it (As the example on the issue page shows)
The correct way according to the BB team, is to either throw a new error completely, or derive a new instance from the one passed to the promise, and alter the parameters to it before then re-throwing it.
Of course, because of the abstraction in Aurelia, this is not an option for most of us, unless we want to go about changing the http client library code.
Some of the marks for this need to go to "TheBlueFox" for His/Her comments above.
The solution ended up being something like the following:
import { inject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
import { HttpClient, Interceptor } from 'aurelia-http-client';
import environment from './environment';
import Debugger = require("_debugger");
#inject(HttpClient, Router)
export default class httpservice {
private http: HttpClient = null;
private router: Router = null;
private authService: any = null;
private authToken: string = "";
private myInterceptors: Interceptor;
constructor(HttpClient, Router) {
this.http = HttpClient;
this.router = Router;
HttpClient.configure(http => {
http.withBaseUrl(environment.servicebase);
http.withInterceptor(new HttpInterceptors());
});
}
public setAuthService(authService: any) {
this.authService = authService;
}
public get(url: string, authObject?: any): any {
let myAuth = this.authService ? this.authService : authObject;
let myToken = "";
if (myAuth) {
myToken = myAuth.getAuthToken();
}
let client = this.http
.createRequest(url)
.asGet()
.withHeader("AuthenticationToken", myToken);
return client;
}
public post(url: string, postData: any, authObject?: any): any {
let myAuth = this.authService ? this.authService : authObject;
let myToken = "";
if (myAuth) {
myToken = myAuth.getAuthToken();
}
let self = this;
let client = this.http
.createRequest(url)
.asPost().withContent(postData)
.withHeader("AuthenticationToken", myToken);
return client;
}
}
class HttpInterceptors implements Interceptor {
responceError(error)
{
if (error.statusCode === 0) {
throw new Error("Could not contact server");
}
if (error.statusCode === 401) {
// do auth handling here
}
if (error.statusCode === 404) {
// do 404 handling here
}
return error;
}
}
The magic is in the HttpInterceptors class attached to the bottom of my HttpService. You should be able to see a check for a status code of 0, and that the actual action performed here is to throw a new error.
It's the action of this new error being thrown that then causes the "catch" in the actual call to the http client to be caught.
If you don't throw at that point, then everything just falls apart and you get the scenario seen in my original question post, throw and you get to catch it and deal with it in user code.
This way of doing things is also apparent in the aurelia-fetch-client too, as that works in a broadly similar way, using the BlueBird promise library.

Resources