From 36600bb1f446a1e5fe3912607b4be91b0742b40a Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Fri, 21 Aug 2020 19:31:24 +0200 Subject: [PATCH] Better error handling --- app/src/main/kotlin/SimpleNotes.kt | 11 +++--- app/src/main/kotlin/filters/ErrorFilter.kt | 21 ++++++++---- app/src/main/kotlin/routes/Router.kt | 3 +- app/src/main/kotlin/views/ErrorView.kt | 34 +++++++++++++++++++ .../main/kotlin/views/components/Alerts.kt | 7 ++-- search/src/main/kotlin/NoteSearcherImpl.kt | 7 +++- 6 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 app/src/main/kotlin/views/ErrorView.kt diff --git a/app/src/main/kotlin/SimpleNotes.kt b/app/src/main/kotlin/SimpleNotes.kt index b73389b..4df0b4d 100644 --- a/app/src/main/kotlin/SimpleNotes.kt +++ b/app/src/main/kotlin/SimpleNotes.kt @@ -6,13 +6,11 @@ import be.simplenotes.app.controllers.SettingsController import be.simplenotes.app.controllers.UserController import be.simplenotes.app.filters.AuthFilter import be.simplenotes.app.filters.AuthType +import be.simplenotes.app.filters.ErrorFilter import be.simplenotes.app.routes.Router import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolverImpl -import be.simplenotes.app.views.BaseView -import be.simplenotes.app.views.NoteView -import be.simplenotes.app.views.SettingView -import be.simplenotes.app.views.UserView +import be.simplenotes.app.views.* import be.simplenotes.domain.domainModule import be.simplenotes.domain.usecases.NoteService import be.simplenotes.persistance.DbMigrations @@ -20,8 +18,10 @@ import be.simplenotes.persistance.persistanceModule import be.simplenotes.search.searchModule import be.simplenotes.shared.config.DataSourceConfig import be.simplenotes.shared.config.JwtConfig +import org.http4k.core.Filter import org.http4k.core.RequestContexts import org.koin.core.context.startKoin +import org.koin.core.qualifier.named import org.koin.core.qualifier.qualifier import org.koin.dsl.module import org.slf4j.LoggerFactory @@ -71,6 +71,7 @@ val serverModule = module { get(), requiredAuth = get(AuthType.Required.qualifier), optionalAuth = get(AuthType.Optional.qualifier), + errorFilter = get(named("ErrorFilter")), get() )() } @@ -78,6 +79,8 @@ val serverModule = module { single { RequestContexts() } single(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get())() } single(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get())() } + single(named("ErrorFilter")) { ErrorFilter(get())() } + single { ErrorView(get()) } } val userModule = module { diff --git a/app/src/main/kotlin/filters/ErrorFilter.kt b/app/src/main/kotlin/filters/ErrorFilter.kt index 6178364..9fd0b97 100644 --- a/app/src/main/kotlin/filters/ErrorFilter.kt +++ b/app/src/main/kotlin/filters/ErrorFilter.kt @@ -1,25 +1,32 @@ package be.simplenotes.app.filters -import org.http4k.core.Filter -import org.http4k.core.Response -import org.http4k.core.Status +import be.simplenotes.app.extensions.html +import be.simplenotes.app.views.ErrorView +import org.http4k.core.* import org.slf4j.LoggerFactory +import java.sql.SQLTransientException + +class ErrorFilter(private val errorView: ErrorView) { -object ErrorFilter { private val logger = LoggerFactory.getLogger(javaClass) operator fun invoke(): Filter = Filter { next -> { try { val response = next(it) - if (response.status == Status.NOT_FOUND) Response(Status.NOT_FOUND).body("TODO(NOT_FOUND)") + if (response.status == Status.NOT_FOUND) Response(Status.NOT_FOUND) + .html(errorView.error(ErrorView.Type.NotFound)) else response } catch (e: Exception) { logger.error(e.stackTraceToString()) - Response(Status.INTERNAL_SERVER_ERROR).body("TODO(INTERNAL_SERVER_ERROR)") + if (e is SQLTransientException) + Response(Status.SERVICE_UNAVAILABLE).html(errorView.error(ErrorView.Type.SqlTransientError)) + .noCache() + else + Response(Status.INTERNAL_SERVER_ERROR).html(errorView.error(ErrorView.Type.Other)).noCache() } catch (e: NotImplementedError) { logger.error(e.stackTraceToString()) - Response(Status.INTERNAL_SERVER_ERROR).body("TODO(NotImplementedError)") + Response(Status.NOT_IMPLEMENTED).html(errorView.error(ErrorView.Type.Other)).noCache() } } } diff --git a/app/src/main/kotlin/routes/Router.kt b/app/src/main/kotlin/routes/Router.kt index a96db09..3c927b8 100644 --- a/app/src/main/kotlin/routes/Router.kt +++ b/app/src/main/kotlin/routes/Router.kt @@ -23,6 +23,7 @@ class Router( private val settingsController: SettingsController, private val requiredAuth: Filter, private val optionalAuth: Filter, + private val errorFilter: Filter, private val contexts: RequestContexts, ) { operator fun invoke(): RoutingHttpHandler { @@ -65,7 +66,7 @@ class Router( requiredAuth.then(protectedRoutes), ) - val globalFilters = ErrorFilter() + val globalFilters = errorFilter .then(InitialiseRequestContext(contexts)) .then(SecurityFilter()) .then(ResponseFilters.GZip()) diff --git a/app/src/main/kotlin/views/ErrorView.kt b/app/src/main/kotlin/views/ErrorView.kt new file mode 100644 index 0000000..4183fc6 --- /dev/null +++ b/app/src/main/kotlin/views/ErrorView.kt @@ -0,0 +1,34 @@ +package be.simplenotes.app.views + +import be.simplenotes.app.utils.StaticFileResolver +import be.simplenotes.app.views.components.Alert +import be.simplenotes.app.views.components.alert +import kotlinx.html.FlowContent +import kotlinx.html.a +import kotlinx.html.div + +class ErrorView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { + + enum class Type(val title: String) { + SqlTransientError("Database unavailable"), + NotFound("Not Found"), + Other("Error"), + } + + fun error(errorType: Type) = renderPage(errorType.title, jwtPayload = null) { + div("container mx-auto p-4") { + when (errorType) { + Type.SqlTransientError -> alert(Alert.Warning, + errorType.title, + "Please try again later", + multiline = true) + Type.NotFound -> alert(Alert.Warning, errorType.title, "Page not found", multiline = true) + Type.Other -> alert(Alert.Warning, errorType.title) + } + div { + a(href = "/", classes = "btn btn-green") { +"Go back to the homepage" } + } + } + } + +} diff --git a/app/src/main/kotlin/views/components/Alerts.kt b/app/src/main/kotlin/views/components/Alerts.kt index 4893be2..7fcb929 100644 --- a/app/src/main/kotlin/views/components/Alerts.kt +++ b/app/src/main/kotlin/views/components/Alerts.kt @@ -2,7 +2,7 @@ package be.simplenotes.app.views.components import kotlinx.html.* -fun FlowContent.alert(type: Alert, title: String, details: String? = null) { +fun FlowContent.alert(type: Alert, title: String, details: String? = null, multiline: Boolean = false) { val colors = when (type) { Alert.Success -> "bg-green-500 border border-green-400 text-gray-800" Alert.Warning -> "bg-red-500 border border-red-400 text-red-200" @@ -10,7 +10,10 @@ fun FlowContent.alert(type: Alert, title: String, details: String? = null) { div("$colors px-4 py-3 mb-4 rounded relative") { attributes["role"] = "alert" strong("font-bold") { +title } - details?.let { span("block sm:inline") { +details } } + details?.let { + if (multiline) p { +details } + else span("block sm:inline") { +details } + } } } diff --git a/search/src/main/kotlin/NoteSearcherImpl.kt b/search/src/main/kotlin/NoteSearcherImpl.kt index c38b7a0..a9937bb 100644 --- a/search/src/main/kotlin/NoteSearcherImpl.kt +++ b/search/src/main/kotlin/NoteSearcherImpl.kt @@ -1,6 +1,7 @@ package be.simplenotes.search import be.simplenotes.domain.model.PersistedNote +import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.domain.usecases.search.NoteSearcher import be.simplenotes.domain.usecases.search.SearchTerms import be.simplenotes.search.utils.rmdir @@ -80,13 +81,17 @@ class NoteSearcherImpl(basePath: Path = Path.of("/tmp", "lucene")) : NoteSearche indexNote(userId, note) } - override fun search(userId: Int, terms: SearchTerms) = + override fun search(userId: Int, terms: SearchTerms) = try { indexSearcher(userId).query { or { titleField eq terms.title } or { tagsField eq terms.tag } or { contentField eq terms.content } listOf(titleField, tagsField, contentField) anyMatch terms.all }.map(Document::toNoteMeta) + } catch (e: IndexNotFoundException) { + logger.warn("Index not found for user $userId") + emptyList() + } override fun dropIndex(userId: Int) = rmdir(File(baseFile, userId.toString()).toPath())