From 286f909ebf11a4641605ffde822f0ac219793356 Mon Sep 17 00:00:00 2001 From: davidl Date: Tue, 10 Mar 2026 21:24:57 +0100 Subject: [PATCH 1/3] Don't log unhandled errors failures of type response --- .../src/test/scala/zio/http/RouteSpec.scala | 23 ++++++++++++++ .../zio/http/ServerErrorLoggingSpec.scala | 30 +++++++++++++++++++ .../src/main/scala/zio/http/Handler.scala | 11 ++++++- .../src/main/scala/zio/http/Route.scala | 11 ++++++- 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/zio-http/jvm/src/test/scala/zio/http/RouteSpec.scala b/zio-http/jvm/src/test/scala/zio/http/RouteSpec.scala index accae22bc4..553c6bc28c 100644 --- a/zio-http/jvm/src/test/scala/zio/http/RouteSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/RouteSpec.scala @@ -88,6 +88,29 @@ object RouteSpec extends ZIOHttpSpec { ), ) }, + test("sandbox does not log requests with failures of type Response") { + val route = + Method.GET / "foo" -> Handler.fail(Response.badRequest) + + for { + _ <- route.sandbox.toRoutes.runZIO(Request.get(url"/foo")) + entries <- ZTestLogger.logOutput + } yield assertTrue( + !entries.exists(e => e.message().contains("Unhandled exception in request handler")), + ) + }, + test("sandbox does not log requests with middleware failures of type Response") { + val route = + Method.GET / "foo" -> handler(Response.ok) + + val routes = route.sandbox.toRoutes @@ Middleware.fail(Response.badRequest) + for { + _ <- routes.runZIO(Request.get(url"/foo")) + entries <- ZTestLogger.logOutput + } yield assertTrue( + !entries.exists(e => e.message().contains("Unhandled exception in request handler")), + ) + }, test("sandbox does not log for successful requests") { val route = Method.GET / "foo" -> handler(Response.ok) diff --git a/zio-http/jvm/src/test/scala/zio/http/ServerErrorLoggingSpec.scala b/zio-http/jvm/src/test/scala/zio/http/ServerErrorLoggingSpec.scala index 9dd9307ad5..91ae8dd922 100644 --- a/zio-http/jvm/src/test/scala/zio/http/ServerErrorLoggingSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/ServerErrorLoggingSpec.scala @@ -39,6 +39,16 @@ object ServerErrorLoggingSpec extends ZIOSpecDefault { Method.GET / "fail" -> Handler.fail(new RuntimeException("typed failure")), ).sandbox + // Routes that fail with a response, should not produce error logs + val routesFailResponse = Routes( + Method.GET / "fail-response" -> Handler.fail(Response.fromThrowable(new RuntimeException("typed failure"))), + ).sandbox + + // Routes that fail with a response because of Middleware, should not produce error logs + val routesFailMiddlewareResponse = (Routes( + Method.GET / "fail-middleware-response" -> Handler.ok, + ) @@ Middleware.fail(Response.fromThrowable(new RuntimeException("typed failure")))).sandbox + // Routes that succeed - should not produce error logs val routesOk = Routes( Method.GET / "ok" -> Handler.ok, @@ -72,6 +82,26 @@ object ServerErrorLoggingSpec extends ZIOSpecDefault { errorLog.get.logLevel == LogLevel.Error, ) }, + test("sandbox does not log requests with failures of type Response") { + for { + port <- Server.installRoutes(routesFailResponse) + _ <- ZIO.scoped { + Client.streaming(Request.get(s"http://localhost:$port/fail-response")).flatMap(_.ignoreBody) + } + entries <- ZTestLogger.logOutput + errorLog = entries.find(_.message() == "Unhandled exception in request handler") + } yield assertTrue(errorLog.isEmpty) + }, + test("sandbox does not log requests with middleware failures of type Response") { + for { + port <- Server.installRoutes(routesFailMiddlewareResponse) + _ <- ZIO.scoped { + Client.streaming(Request.get(s"http://localhost:$port/fail-middleware-response")).flatMap(_.ignoreBody) + } + entries <- ZTestLogger.logOutput + errorLog = entries.find(_.message() == "Unhandled exception in request handler") + } yield assertTrue(errorLog.isEmpty) + }, test("sandbox does not log for successful requests") { for { port <- Server.installRoutes(routesOk) diff --git a/zio-http/shared/src/main/scala/zio/http/Handler.scala b/zio-http/shared/src/main/scala/zio/http/Handler.scala index 9363c88f7e..cfe35b9767 100644 --- a/zio-http/shared/src/main/scala/zio/http/Handler.scala +++ b/zio-http/shared/src/main/scala/zio/http/Handler.scala @@ -550,10 +550,19 @@ sealed trait Handler[-R, +Err, -In, +Out] { self => final def sandbox(implicit trace: Trace): Handler[R, Response, In, Out] = self.mapErrorCauseZIO { cause => - ZIO.logErrorCause("Unhandled exception in request handler", cause) *> + logUnhandledError(cause) *> ErrorResponseConfig.configRef.getWith(cfg => Exit.fail(Response.fromCause(cause, cfg))) } + /** + * Log an unhandled error unless the cause is already a response + */ + private def logUnhandledError(cause: Cause[Any]): UIO[Unit] = + cause.failureOrCause match { + case Left(_: Response) => ZIO.unit + case _ => ZIO.logErrorCause("Unhandled exception in request handler", cause) + } + final def status(implicit ev: Out <:< Response, trace: Trace): Handler[R, Err, In, Status] = self.map(_.status) diff --git a/zio-http/shared/src/main/scala/zio/http/Route.scala b/zio-http/shared/src/main/scala/zio/http/Route.scala index 0d7dde253d..b2519da01f 100644 --- a/zio-http/shared/src/main/scala/zio/http/Route.scala +++ b/zio-http/shared/src/main/scala/zio/http/Route.scala @@ -410,10 +410,19 @@ sealed trait Route[-Env, +Err] { self => */ final def sandbox(implicit trace: Trace): Route[Env, Nothing] = handleErrorCauseZIO { cause => - ZIO.logErrorCause("Unhandled exception in request handler", cause) *> + logUnhandledError(cause) *> ErrorResponseConfig.configRef.getWith(cfg => Exit.succeed(Response.fromCause(cause, cfg))) } + /** + * Log an unhandled error unless the cause is already a response + */ + private def logUnhandledError(cause: Cause[Any]): UIO[Unit] = + cause.failureOrCause match { + case Left(_: Response) => ZIO.unit + case _ => ZIO.logErrorCause("Unhandled exception in request handler", cause) + } + def toHandler(implicit ev: Err <:< Response, trace: Trace): Handler[Env, Response, Request, Response] /** From 6e40a44af11cc2033f315e2ea9d9bcd9578b7c6b Mon Sep 17 00:00:00 2001 From: davidl Date: Tue, 10 Mar 2026 22:58:12 +0100 Subject: [PATCH 2/3] Scalafmt --- zio-http/shared/src/main/scala/zio/http/Handler.scala | 2 +- zio-http/shared/src/main/scala/zio/http/Route.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zio-http/shared/src/main/scala/zio/http/Handler.scala b/zio-http/shared/src/main/scala/zio/http/Handler.scala index cfe35b9767..f247f80d87 100644 --- a/zio-http/shared/src/main/scala/zio/http/Handler.scala +++ b/zio-http/shared/src/main/scala/zio/http/Handler.scala @@ -560,7 +560,7 @@ sealed trait Handler[-R, +Err, -In, +Out] { self => private def logUnhandledError(cause: Cause[Any]): UIO[Unit] = cause.failureOrCause match { case Left(_: Response) => ZIO.unit - case _ => ZIO.logErrorCause("Unhandled exception in request handler", cause) + case _ => ZIO.logErrorCause("Unhandled exception in request handler", cause) } final def status(implicit ev: Out <:< Response, trace: Trace): Handler[R, Err, In, Status] = diff --git a/zio-http/shared/src/main/scala/zio/http/Route.scala b/zio-http/shared/src/main/scala/zio/http/Route.scala index b2519da01f..50fc3105a6 100644 --- a/zio-http/shared/src/main/scala/zio/http/Route.scala +++ b/zio-http/shared/src/main/scala/zio/http/Route.scala @@ -420,7 +420,7 @@ sealed trait Route[-Env, +Err] { self => private def logUnhandledError(cause: Cause[Any]): UIO[Unit] = cause.failureOrCause match { case Left(_: Response) => ZIO.unit - case _ => ZIO.logErrorCause("Unhandled exception in request handler", cause) + case _ => ZIO.logErrorCause("Unhandled exception in request handler", cause) } def toHandler(implicit ev: Err <:< Response, trace: Trace): Handler[Env, Response, Request, Response] From 14bb780aec8427de2de5414624a29ff6ec6f1352 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:29:24 +0000 Subject: [PATCH 3/3] Update tapir-http4s-server, ... to 1.13.13 --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 083dd10e6e..575e3e1564 100644 --- a/build.sbt +++ b/build.sbt @@ -279,8 +279,8 @@ lazy val zioHttpBenchmarks = (project in file("zio-http-benchmarks")) .settings( libraryDependencies ++= Seq( // "com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.1.0", - "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.13.10", - "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.13.10", + "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.13.13", + "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.13.13", "com.softwaremill.sttp.client3" %% "core" % "3.11.0", // "dev.zio" %% "zio-interop-cats" % "3.3.0", "org.slf4j" % "slf4j-api" % "2.0.17",