Better error handling
This commit is contained in:
parent
b27fd29230
commit
36600bb1f4
@ -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 {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
34
app/src/main/kotlin/views/ErrorView.kt
Normal file
34
app/src/main/kotlin/views/ErrorView.kt
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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())
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user