From c44912b6a3bdc80699e6cb7d51a7e6e5d56ed587 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:42:58 +0200 Subject: [PATCH] Add docs for ErrorResponseConfig (#2198) (#3175) --- docs/reference/handler.md | 11 ++++ docs/reference/response/response.md | 50 +++++++++++++++++++ .../scala/zio/http/ErrorResponseConfig.scala | 7 +++ 3 files changed, 68 insertions(+) diff --git a/docs/reference/handler.md b/docs/reference/handler.md index 52bddff4e4..f4b43aedf5 100644 --- a/docs/reference/handler.md +++ b/docs/reference/handler.md @@ -612,6 +612,17 @@ In this example, the type of the handler before applying the `sandbox` operator Without the `sandbox` operator, the compiler would complain about the unhandled `Throwable` error. +By default, sandboxed errors will result in a `500 Internal Server Error` response without a body. If you want to have all information about the error in the response body you can use a different (`ErrorResponseConfig`)[response/response.md#failure-responses-with-details] like `ErrorResponseConfig.debug`: + +```scala mdoc:compile-only +import zio.http._ +import java.nio.file._ + +Routes( + Method.GET / "file" -> + Handler.fromFile(Paths.get("file.txt").toFile).sandbox, + ) @@ ErrorResponseConfig.debug +``` ### Converting a `Handler` to an `Routes` The `Handler#toRoutes` operator, converts a handler to an `Routes` to be served by the `Server`. The following example, shows an HTTP application that serves a simple "Hello, World!" response for all types of incoming requests: diff --git a/docs/reference/response/response.md b/docs/reference/response/response.md index cb6a3ce842..f5d0dec5dc 100644 --- a/docs/reference/response/response.md +++ b/docs/reference/response/response.md @@ -188,6 +188,56 @@ val failedHandler = Handler.fail(new IOException()) failedHandler.mapErrorCause(Response.fromCause) ``` +#### Failure Responses with Details + +By default, the `Response.fromThrowable` and `Response.fromCause` methods create a response with a status code only. If we want to include additional details in the response, we have to hand over a `ErrorResponseConfig`. + +```scala +/** + * Configuration for the response generation + * + * @param withErrorBody + * if true, includes the error message in the response body + * @param withStackTrace + * if true, includes the stack trace in the response body + * @param maxStackTraceDepth + * maximum number of stack trace lines to include in the response body. Set to + * 0 to include all lines. + * @param errorFormat + * the preferred format for the error response. + * If the context in which the response is created has access to an Accept header, + * the header will be used preferably to determine the format. + */ +final case class ErrorResponseConfig( + withErrorBody: Boolean = false, + withStackTrace: Boolean = false, + maxStackTraceDepth: Int = 10, + errorFormat: ErrorResponseConfig.ErrorFormat = ErrorResponseConfig.ErrorFormat.Html, +) +``` + +This config can not only be used directly, but can also configure how ZIO-HTTP internally converts a `Cause` or `Throwable` to a `Response`. +You can configure error responses globally by providing a custom `ErrorResponseConfig` via layer for example in the bootstrap of your application. +Or you can apply the config locally to some routes via middleware. + +```scala mdoc +import zio.http._ + +object MyHttpApp extends ZIOAppDefault { + // Provide a custom ErrorResponseConfig via layer + // Equivalent to: val bootstrap = ErrorResponseConfig.configLayer(ErrorResponseConfig.debugConfig) + override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = ErrorResponseConfig.debugLayer + + // Apply the ErrorResponseConfig.debug middleware to routes + // Equivalent to: val myRoutes = Handler.ok.toRoutes @@ ErrorResponseConfig.withConfig(ErrorResponseConfig.debugConfig) + val myRoutes = Handler.ok.toRoutes @@ ErrorResponseConfig.debug + + override def run = ??? +} +``` + +The debug config will include the error message and full stack trace in the response body. + :::note In many cases, it is more convenient to use the `sandbox` method to automatically convert all failures into a corresponding `Response`. But in some cases, to have more granular control over the error handling, we may want to use `Response.fromCause` and `Response.fromThrowable` directly. ::: diff --git a/zio-http/shared/src/main/scala/zio/http/ErrorResponseConfig.scala b/zio-http/shared/src/main/scala/zio/http/ErrorResponseConfig.scala index bc9a3fa8d5..10237403a8 100644 --- a/zio-http/shared/src/main/scala/zio/http/ErrorResponseConfig.scala +++ b/zio-http/shared/src/main/scala/zio/http/ErrorResponseConfig.scala @@ -12,6 +12,10 @@ import zio._ * @param maxStackTraceDepth * maximum number of stack trace lines to include in the response body. Set to * 0 to include all lines. + * @param errorFormat + * the preferred format for the error response. If the context in which the + * response is created has access to an Accept header, the header will be used + * preferably to determine the format. */ final case class ErrorResponseConfig( withErrorBody: Boolean = false, @@ -38,6 +42,9 @@ object ErrorResponseConfig { val debug: HandlerAspect[Any, Unit] = Middleware.runBefore(setConfig(debugConfig)) + val debugLayer: ULayer[Unit] = + ZLayer(setConfig(debugConfig)) + def withConfig(config: ErrorResponseConfig): HandlerAspect[Any, Unit] = Middleware.runBefore(setConfig(config))