Skip to content

Commit

Permalink
[428] Better error handling in Xef Server (#430)
Browse files Browse the repository at this point in the history
* added exceptions handler management in server

* removed unnecessary response
  • Loading branch information
Montagon authored Sep 18, 2023
1 parent 44d9ad7 commit 8155a54
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 97 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ ktor-server-contentNegotiation = { module = "io.ktor:ktor-server-content-negotia
ktor-server-resources = { module = "io.ktor:ktor-server-resources", version.ref = "ktor" }
ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
ktor-server-request-validation = { module = "io.ktor:ktor-server-request-validation", version.ref = "ktor" }
ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" }
okio-nodefilesystem = { module = "com.squareup.okio:okio-nodefilesystem", version.ref = "okio" }
Expand Down
1 change: 1 addition & 0 deletions server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies {
implementation(libs.ktor.server.resources)
implementation(libs.ktor.server.cors)
implementation(libs.ktor.server.request.validation)
implementation(libs.ktor.server.status.pages)
implementation(libs.logback)
implementation(libs.openai.client)
implementation(libs.suspendApp.core)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.xebia.functional.xef.server

import arrow.continuations.SuspendApp
import arrow.fx.coroutines.resourceScope
import arrow.continuations.ktor.server
import arrow.fx.coroutines.resourceScope
import com.typesafe.config.ConfigFactory
import com.xebia.functional.xef.server.db.psql.XefDatabaseConfig
import com.xebia.functional.xef.server.db.psql.Migrate
import com.xebia.functional.xef.server.db.psql.XefDatabaseConfig
import com.xebia.functional.xef.server.db.psql.XefVectorStoreConfig
import com.xebia.functional.xef.server.db.psql.XefVectorStoreConfig.Companion.getVectorStoreService
import com.xebia.functional.xef.server.exceptions.exceptionsHandler
import com.xebia.functional.xef.server.http.routes.genAIRoutes
import com.xebia.functional.xef.server.http.routes.organizationRoutes
import com.xebia.functional.xef.server.http.routes.userRoutes
Expand All @@ -16,7 +17,6 @@ import com.xebia.functional.xef.server.services.RepositoryService
import com.xebia.functional.xef.server.services.UserRepositoryService
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.logging.*
import io.ktor.http.*
Expand All @@ -31,6 +31,7 @@ import io.ktor.server.routing.*
import kotlinx.coroutines.awaitCancellation
import org.jetbrains.exposed.sql.Database
import org.slf4j.LoggerFactory
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation

object Server {
@JvmStatic
Expand Down Expand Up @@ -80,6 +81,7 @@ object Server {
}
}
}
exceptionsHandler()
routing {
genAIRoutes(ktorClient, vectorStoreService)
userRoutes(UserRepositoryService(logger))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.xebia.functional.xef.server.exceptions

import com.xebia.functional.xef.server.models.exceptions.XefExceptions
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*

fun Application.exceptionsHandler() {
install(StatusPages) {
exception<Throwable> { call, cause ->
if (cause is XefExceptions) {
call.manageException(cause)
} else {
call.respond(HttpStatusCode.InternalServerError, cause.localizedMessage ?: "Unexpected error")
}
}
status(HttpStatusCode.NotFound) { call, status ->
call.respondText(text = "404: Page Not Found", status = status)
}
}
}

suspend fun ApplicationCall.manageException(cause: XefExceptions) {
when(cause) {
is XefExceptions.ValidationException -> this.respond(HttpStatusCode.BadRequest, cause.message)
is XefExceptions.AuthorizationException -> this.respond(HttpStatusCode.Unauthorized)
is XefExceptions.OrganizationsException -> this.respond(HttpStatusCode.BadRequest, cause.message)
is XefExceptions.UserException -> this.respond(HttpStatusCode.BadRequest, cause.message)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.xebia.functional.xef.server.http.routes

import com.xebia.functional.xef.server.models.OrganizationRequest
import com.xebia.functional.xef.server.models.OrganizationUpdateRequest
import com.xebia.functional.xef.server.models.exceptions.XefExceptions
import com.xebia.functional.xef.server.services.OrganizationRepositoryService
import io.ktor.http.*
import io.ktor.server.application.*
Expand All @@ -16,74 +17,56 @@ fun Routing.organizationRoutes(
) {
authenticate("auth-bearer") {
get("/v1/settings/org") {
try {
val token = call.getToken()
val response = orgRepositoryService.getOrganizations(token)
call.respond(response)
} catch (e: Exception) {
call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest)
}
val token = call.getToken()
val response = orgRepositoryService.getOrganizations(token)
call.respond(response)
}
get("/v1/settings/org/{id}") {
try {
val token = call.getToken()
val id = call.parameters["id"]?.toInt() ?: throw Exception("Invalid id")
val response = orgRepositoryService.getOrganization(token, id)
call.respond(response)
} catch (e: Exception) {
call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest)
}

val token = call.getToken()
val id = call.getOrganizationId()
val response = orgRepositoryService.getOrganization(token, id)
call.respond(response)
}
get("/v1/settings/org/{id}/users"){
try {
val token = call.getToken()
val id = call.parameters["id"]?.toInt() ?: throw Exception("Invalid id")
val response = orgRepositoryService.getUsersInOrganization(token, id)
call.respond(response)
} catch (e: Exception) {
call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest)
}
get("/v1/settings/org/{id}/users") {
val token = call.getToken()
val id = call.getOrganizationId()
val response = orgRepositoryService.getUsersInOrganization(token, id)
call.respond(response)
}
post("/v1/settings/org") {
try {
val request = Json.decodeFromString<OrganizationRequest>(call.receive<String>())
val token = call.getToken()
val response = orgRepositoryService.createOrganization(request, token)
call.respond(
status = HttpStatusCode.Created,
response
)
} catch (e: Exception) {
call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest)
}

val request = Json.decodeFromString<OrganizationRequest>(call.receive<String>())
val token = call.getToken()
val response = orgRepositoryService.createOrganization(request, token)
call.respond(
status = HttpStatusCode.Created,
response
)
}
put("/v1/settings/org/{id}") {
try {
val request = Json.decodeFromString<OrganizationUpdateRequest>(call.receive<String>())
val token = call.getToken()
val id = call.parameters["id"]?.toInt() ?: throw Exception("Invalid id")
val response = orgRepositoryService.updateOrganization(token, request, id)
call.respond(
status = HttpStatusCode.NoContent,
response
)
} catch (e: Exception) {
call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest)
}
val request = Json.decodeFromString<OrganizationUpdateRequest>(call.receive<String>())
val token = call.getToken()
val id = call.getOrganizationId()
val response = orgRepositoryService.updateOrganization(token, request, id)
call.respond(
status = HttpStatusCode.NoContent,
response
)
}
delete("/v1/settings/org/{id}") {
try {
val token = call.getToken()
val id = call.parameters["id"]?.toInt() ?: throw Exception("Invalid id")
val response = orgRepositoryService.deleteOrganization(token, id)
call.respond(
status = HttpStatusCode.NoContent,
response
)
} catch (e: Exception) {
call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest)
}
val token = call.getToken()
val id = call.getOrganizationId()
val response = orgRepositoryService.deleteOrganization(token, id)
call.respond(
status = HttpStatusCode.NoContent,
response
)
}
}
}

private fun ApplicationCall.getOrganizationId(): Int {
return this.parameters["id"]?.toInt() ?: throw XefExceptions.ValidationException("Invalid id")
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,14 @@ fun Routing.userRoutes(
userRepositoryService: UserRepositoryService
) {
post("/register") {
try {
val request = Json.decodeFromString<RegisterRequest>(call.receive<String>())
val response = userRepositoryService.register(request)
call.respond(response)
} catch (e: Exception) {
call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest)
}
val request = Json.decodeFromString<RegisterRequest>(call.receive<String>())
val response = userRepositoryService.register(request)
call.respond(response)
}

post("/login") {
try {
val request = Json.decodeFromString<LoginRequest>(call.receive<String>())
val response = userRepositoryService.login(request)
call.respond(response)
} catch (e: Exception) {
call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest)
}
val request = Json.decodeFromString<LoginRequest>(call.receive<String>())
val response = userRepositoryService.login(request)
call.respond(response)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.xebia.functional.xef.server.http.routes

import com.aallam.openai.api.BetaOpenAI
import com.xebia.functional.xef.server.models.exceptions.XefExceptions
import com.xebia.functional.xef.server.services.VectorStoreService
import io.ktor.client.*
import io.ktor.client.call.*
Expand All @@ -12,7 +13,6 @@ import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import io.ktor.utils.io.jvm.javaio.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
Expand Down Expand Up @@ -113,16 +113,4 @@ private fun ApplicationCall.getProvider(): Provider =
?: Provider.OPENAI

fun ApplicationCall.getToken(): String =
principal<UserIdPrincipal>()?.name ?: throw IllegalArgumentException("No token found")


/**
* Responds with the data and converts any potential Throwable into a 404.
*/
private suspend inline fun <reified T : Any, E : Throwable> PipelineContext<*, ApplicationCall>.response(
block: () -> T
) = arrow.core.raise.recover<E, Unit>({
call.respond(block())
}) {
call.respondText(it.message ?: "Response not found", status = HttpStatusCode.NotFound)
}
principal<UserIdPrincipal>()?.name ?: throw XefExceptions.AuthorizationException("No token found")
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.xebia.functional.xef.server.models.exceptions

sealed class XefExceptions(
override val message: String
): Throwable() {
class ValidationException(override val message: String): XefExceptions(message)
class OrganizationsException(override val message: String): XefExceptions(message)
class AuthorizationException(override val message: String): XefExceptions(message)
class UserException(override val message: String): XefExceptions(message)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.xebia.functional.xef.server.db.tables.Organization
import com.xebia.functional.xef.server.db.tables.User
import com.xebia.functional.xef.server.db.tables.UsersTable
import com.xebia.functional.xef.server.models.*
import com.xebia.functional.xef.server.models.exceptions.XefExceptions.*
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.SizedCollection
import org.jetbrains.exposed.sql.transactions.transaction
Expand Down Expand Up @@ -89,17 +90,17 @@ class OrganizationRepositoryService(
val user = getUser(token)

val organization = Organization.findById(id)
?: throw Exception("Organization not found")
?: throw OrganizationsException("Organization not found")

if (organization.ownerId != user.id) {
throw Exception("User is not the owner of the organization")
throw OrganizationsException("User is not the owner of the organization")
}

// Updating the organization
organization.name = data.name
if (data.owner != null) {
val newOwner = User.findById(data.owner)
?: throw Exception("User not found")
?: throw UserException("User not found")
organization.ownerId = newOwner.id
}
organization.updatedAt = Clock.System.now()
Expand All @@ -120,13 +121,13 @@ class OrganizationRepositoryService(
transaction {
val user = getUser(token)
val organization = Organization.findById(id)
?: throw Exception("Organization not found")
?: throw OrganizationsException("Organization not found")
if (organization.ownerId == user.id) {
organization.delete()
}
}
}

private fun getUser(token: String) =
User.find { UsersTable.authToken eq token }.firstOrNull() ?: throw Exception("User not found")
User.find { UsersTable.authToken eq token }.firstOrNull() ?: throw UserException("User not found")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.xebia.functional.xef.server.db.tables.UsersTable
import com.xebia.functional.xef.server.models.LoginRequest
import com.xebia.functional.xef.server.models.LoginResponse
import com.xebia.functional.xef.server.models.RegisterRequest
import com.xebia.functional.xef.server.models.exceptions.XefExceptions.UserException
import com.xebia.functional.xef.server.utils.HashUtils
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.uuid.UUID
import kotlinx.uuid.generateUUID
import org.jetbrains.exposed.sql.transactions.transaction
Expand All @@ -21,7 +21,7 @@ class UserRepositoryService(

return transaction {
if (User.find { UsersTable.email eq request.email }.count() > 0) {
throw Exception("User already exists")
throw UserException("User already exists")
}

val newSalt = HashUtils.generateSalt()
Expand All @@ -43,7 +43,7 @@ class UserRepositoryService(
logger.info("Login user ${request.email}")
return transaction {
val user =
User.find { UsersTable.email eq request.email }.firstOrNull() ?: throw Exception("User not found")
User.find { UsersTable.email eq request.email }.firstOrNull() ?: throw UserException("User not found")

if (!HashUtils.checkPassword(
request.password,
Expand Down

0 comments on commit 8155a54

Please sign in to comment.