How to add custom error responses in Http4s?
Asked Answered
E

2

6

Whenever I hit unknown route in my http4s application it returns 404 error page with Content-Type: text/plain and body:

Not found

How can I force it to always return body as JSON with Content-Type: application/json?

{"message": "Not found"}

I figured out that when I assembly httpApp I can map over it and "adjust" responses:

val httpApp = Router.publicRoutes[F].orNotFound.map(ErrorTranslator.handle)

where ErrorTranslator just detects responses with status code of client error and Content-Type which is not application/json and then just wraps body into JSON:

object ErrorTranslator {

  val ContentType = "Content-Type"
  val ApplicationJson = "application/json"

  private def translate[F[_]: ConcurrentEffect: Sync](r: Response[F]): Response[F] =
    r.headers.get(CaseInsensitiveString(ContentType)).map(_.value) match {
      case Some(ApplicationJson) => r
      case _                     => r.withEntity(r.bodyAsText.map(ErrorView(_))) //wrap reponse body into enity
    }

  def handle[F[_]: ConcurrentEffect: Sync]: PartialFunction[Response[F], Response[F]] = {
    case Status.ClientError(r) => translate(r)
    case r                     => r
  }

}

It works, but I wonder if there is maybe some less convoluted solution?

It would be also great if a solution could "translate" other errors, like 400 Bad request into JSON, similarily to presented code.

Exhilarate answered 29/12, 2019 at 10:33 Comment(0)
D
1

You can also make it with value and mapF function:

val jsonNotFound: Response[F] =
  Response(
    Status.NotFound,
    body = Stream("""{"error": "Not found"}""").through(utf8Encode),
    headers = Headers(`Content-Type`(MediaType.application.json) :: Nil)
  )
val routes: HttpRoutes[F] = routes.someRoutes().mapF(_.getOrElse(jsonNotFound))
Draughtboard answered 28/8, 2020 at 6:28 Comment(1)
I think it's cleaner and shorter that my approach, thanks!Haleakala
C
1

I suppose you have defined your routes in a similar fashion, then you can add a default case statement

 HttpRoutes.of[IO] {
   case GET -> Root / "api" =>
        Ok()

   case _ -> Root =>
        // Your default route could be done like this
        Ok(io.circe.parser.parse("""{"message": "Not Found"}"""))
}
Confront answered 6/1, 2020 at 14:58 Comment(1)
case _ -> _ seems to work, but still it would properly handle other error codes like 400.Haleakala
D
1

You can also make it with value and mapF function:

val jsonNotFound: Response[F] =
  Response(
    Status.NotFound,
    body = Stream("""{"error": "Not found"}""").through(utf8Encode),
    headers = Headers(`Content-Type`(MediaType.application.json) :: Nil)
  )
val routes: HttpRoutes[F] = routes.someRoutes().mapF(_.getOrElse(jsonNotFound))
Draughtboard answered 28/8, 2020 at 6:28 Comment(1)
I think it's cleaner and shorter that my approach, thanks!Haleakala

© 2022 - 2024 — McMap. All rights reserved.