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.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 {
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
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.*
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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())
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user