Better error handling

This commit is contained in:
Hubert Van De Walle 2020-08-21 19:31:24 +02:00
parent b27fd29230
commit 36600bb1f4
6 changed files with 68 additions and 15 deletions

View File

@ -6,13 +6,11 @@ import be.simplenotes.app.controllers.SettingsController
import be.simplenotes.app.controllers.UserController import be.simplenotes.app.controllers.UserController
import be.simplenotes.app.filters.AuthFilter import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.routes.Router import be.simplenotes.app.routes.Router
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.utils.StaticFileResolverImpl import be.simplenotes.app.utils.StaticFileResolverImpl
import be.simplenotes.app.views.BaseView import be.simplenotes.app.views.*
import be.simplenotes.app.views.NoteView
import be.simplenotes.app.views.SettingView
import be.simplenotes.app.views.UserView
import be.simplenotes.domain.domainModule import be.simplenotes.domain.domainModule
import be.simplenotes.domain.usecases.NoteService import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.persistance.DbMigrations import be.simplenotes.persistance.DbMigrations
@ -20,8 +18,10 @@ import be.simplenotes.persistance.persistanceModule
import be.simplenotes.search.searchModule import be.simplenotes.search.searchModule
import be.simplenotes.shared.config.DataSourceConfig import be.simplenotes.shared.config.DataSourceConfig
import be.simplenotes.shared.config.JwtConfig import be.simplenotes.shared.config.JwtConfig
import org.http4k.core.Filter
import org.http4k.core.RequestContexts import org.http4k.core.RequestContexts
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.qualifier.named
import org.koin.core.qualifier.qualifier import org.koin.core.qualifier.qualifier
import org.koin.dsl.module import org.koin.dsl.module
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -71,6 +71,7 @@ val serverModule = module {
get(), get(),
requiredAuth = get(AuthType.Required.qualifier), requiredAuth = get(AuthType.Required.qualifier),
optionalAuth = get(AuthType.Optional.qualifier), optionalAuth = get(AuthType.Optional.qualifier),
errorFilter = get(named("ErrorFilter")),
get() get()
)() )()
} }
@ -78,6 +79,8 @@ val serverModule = module {
single { RequestContexts() } single { RequestContexts() }
single(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get())() } single(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get())() }
single(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get())() } single(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get())() }
single(named("ErrorFilter")) { ErrorFilter(get())() }
single { ErrorView(get()) }
} }
val userModule = module { val userModule = module {

View File

@ -1,25 +1,32 @@
package be.simplenotes.app.filters package be.simplenotes.app.filters
import org.http4k.core.Filter import be.simplenotes.app.extensions.html
import org.http4k.core.Response import be.simplenotes.app.views.ErrorView
import org.http4k.core.Status import org.http4k.core.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.sql.SQLTransientException
class ErrorFilter(private val errorView: ErrorView) {
object ErrorFilter {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
operator fun invoke(): Filter = Filter { next -> operator fun invoke(): Filter = Filter { next ->
{ {
try { try {
val response = next(it) 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 else response
} catch (e: Exception) { } catch (e: Exception) {
logger.error(e.stackTraceToString()) 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) { } catch (e: NotImplementedError) {
logger.error(e.stackTraceToString()) logger.error(e.stackTraceToString())
Response(Status.INTERNAL_SERVER_ERROR).body("TODO(NotImplementedError)") Response(Status.NOT_IMPLEMENTED).html(errorView.error(ErrorView.Type.Other)).noCache()
} }
} }
} }

View File

@ -23,6 +23,7 @@ class Router(
private val settingsController: SettingsController, private val settingsController: SettingsController,
private val requiredAuth: Filter, private val requiredAuth: Filter,
private val optionalAuth: Filter, private val optionalAuth: Filter,
private val errorFilter: Filter,
private val contexts: RequestContexts, private val contexts: RequestContexts,
) { ) {
operator fun invoke(): RoutingHttpHandler { operator fun invoke(): RoutingHttpHandler {
@ -65,7 +66,7 @@ class Router(
requiredAuth.then(protectedRoutes), requiredAuth.then(protectedRoutes),
) )
val globalFilters = ErrorFilter() val globalFilters = errorFilter
.then(InitialiseRequestContext(contexts)) .then(InitialiseRequestContext(contexts))
.then(SecurityFilter()) .then(SecurityFilter())
.then(ResponseFilters.GZip()) .then(ResponseFilters.GZip())

View File

@ -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" }
}
}
}
}

View File

@ -2,7 +2,7 @@ package be.simplenotes.app.views.components
import kotlinx.html.* 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) { val colors = when (type) {
Alert.Success -> "bg-green-500 border border-green-400 text-gray-800" Alert.Success -> "bg-green-500 border border-green-400 text-gray-800"
Alert.Warning -> "bg-red-500 border border-red-400 text-red-200" 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") { div("$colors px-4 py-3 mb-4 rounded relative") {
attributes["role"] = "alert" attributes["role"] = "alert"
strong("font-bold") { +title } strong("font-bold") { +title }
details?.let { span("block sm:inline") { +details } } details?.let {
if (multiline) p { +details }
else span("block sm:inline") { +details }
}
} }
} }

View File

@ -1,6 +1,7 @@
package be.simplenotes.search package be.simplenotes.search
import be.simplenotes.domain.model.PersistedNote 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.NoteSearcher
import be.simplenotes.domain.usecases.search.SearchTerms import be.simplenotes.domain.usecases.search.SearchTerms
import be.simplenotes.search.utils.rmdir import be.simplenotes.search.utils.rmdir
@ -80,13 +81,17 @@ class NoteSearcherImpl(basePath: Path = Path.of("/tmp", "lucene")) : NoteSearche
indexNote(userId, note) indexNote(userId, note)
} }
override fun search(userId: Int, terms: SearchTerms) = override fun search(userId: Int, terms: SearchTerms) = try {
indexSearcher(userId).query { indexSearcher(userId).query {
or { titleField eq terms.title } or { titleField eq terms.title }
or { tagsField eq terms.tag } or { tagsField eq terms.tag }
or { contentField eq terms.content } or { contentField eq terms.content }
listOf(titleField, tagsField, contentField) anyMatch terms.all listOf(titleField, tagsField, contentField) anyMatch terms.all
}.map(Document::toNoteMeta) }.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()) override fun dropIndex(userId: Int) = rmdir(File(baseFile, userId.toString()).toPath())