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.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 {

View File

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

View File

@ -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())

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.*
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 }
}
}
}

View File

@ -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())