Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[428] Better error handling in Xef Server #430

Merged
merged 4 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,32 @@
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.message ?: "Unexpected error")
}
call.respondText(cause.localizedMessage, status = HttpStatusCode.InternalServerError)
Montagon marked this conversation as resolved.
Show resolved Hide resolved
}
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)
Montagon marked this conversation as resolved.
Show resolved Hide resolved
}
}
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