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 82b4b2a3ab..d778c2670e 100644 --- a/zio-http/jvm/src/test/scala/zio/http/RouteSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/RouteSpec.scala @@ -163,6 +163,58 @@ object RouteSpec extends ZIOHttpSpec { bodyString <- response.body.asString } yield assertTrue(extractStatus(response) == Status.InternalServerError, bodyString == "error") }, + test("tapErrorZIO is not called when the route succeeds") { + val route = Method.GET / "endpoint" -> handler { (_: Request) => ZIO.attempt(Response.ok) } + val errorTapped = route.tapErrorZIO(_ => ZIO.log("tapErrorZIO")).sandbox + for { + _ <- errorTapped(Request.get("/endpoint")) + didLog <- ZTestLogger.logOutput.map(out => out.find(_.message() == "tapErrorZIO").isDefined) + } yield assertTrue(!didLog) + }, + test("tapErrorZIO is called when the route fails with an error") { + val route = Method.GET / "endpoint" -> handler { (_: Request) => ZIO.fail(new Exception("hm...")) } + val errorTapped = route.tapErrorZIO(_ => ZIO.log("tapErrorZIO")).sandbox + for { + _ <- errorTapped(Request.get("/endpoint")).sandbox + didLog <- ZTestLogger.logOutput.map(out => out.find(_.message() == "tapErrorZIO").isDefined) + } yield assertTrue(didLog) + }, + test("tapErrorZIO is not called when the route fails with a defect") { + val route: Route[Any, Unit] = Method.GET / "endpoint" -> handler { (_: Request) => + ZIO.die(new Exception("hm...")) + } + val errorTapped = route.tapErrorZIO(_ => ZIO.log("tapErrorZIO")).sandbox + for { + _ <- errorTapped(Request.get("/endpoint")).sandbox + didLog <- ZTestLogger.logOutput.map(out => out.find(_.message() == "tapErrorZIO").isDefined) + } yield assertTrue(!didLog) + }, + test("tapErrorCauseZIO is not called when the route succeeds") { + val route = Method.GET / "endpoint" -> handler { (_: Request) => ZIO.attempt(Response.ok) } + val causeTapped = route.tapErrorCauseZIO(_ => ZIO.log("tapErrorCauseZIO")).sandbox + for { + _ <- causeTapped(Request.get("/endpoint")) + didLog <- ZTestLogger.logOutput.map(out => out.find(_.message() == "tapErrorCauseZIO").isDefined) + } yield assertTrue(!didLog) + }, + test("tapErrorCauseZIO is called when the route fails with an error") { + val route = Method.GET / "endpoint" -> handler { (_: Request) => ZIO.fail(new Exception("hm...")) } + val causeTapped = route.tapErrorCauseZIO(_ => ZIO.log("tapErrorCauseZIO")).sandbox + for { + _ <- causeTapped(Request.get("/endpoint")).sandbox + didLog <- ZTestLogger.logOutput.map(out => out.find(_.message() == "tapErrorCauseZIO").isDefined) + } yield assertTrue(didLog) + }, + test("tapErrorCauseZIO is called when the route fails with a defect") { + val route: Route[Any, Unit] = Method.GET / "endpoint" -> handler { (_: Request) => + ZIO.die(new Exception("hm...")) + } + val causeTapped = route.tapErrorCauseZIO(_ => ZIO.log("tapErrorCauseZIO")).sandbox + for { + _ <- causeTapped(Request.get("/endpoint")).sandbox + didLog <- ZTestLogger.logOutput.map(out => out.find(_.message() == "tapErrorCauseZIO").isDefined) + } yield assertTrue(didLog) + }, ), ) } 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 768a0d10b9..91e3ae3dde 100644 --- a/zio-http/shared/src/main/scala/zio/http/Route.scala +++ b/zio-http/shared/src/main/scala/zio/http/Route.scala @@ -15,8 +15,10 @@ */ package zio.http +import zio.Cause.Fail import zio._ +import zio.http.Route.CheckResponse import zio.http.codec.PathCodec /* @@ -131,6 +133,42 @@ sealed trait Route[-Env, +Err] { self => Handled(rpm.routePattern, handler2, location) } + /** + * Effectfully peeks at the unhandled failure of this Route. + */ + final def tapErrorZIO[Err1 >: Err]( + f: Err => ZIO[Any, Err1, Any], + )(implicit trace: Trace, ev: CheckResponse[Err]): Route[Env, Err1] = + self match { + case Provided(route, env) => Provided(route.tapErrorZIO(f), env) + case Augmented(route, aspect) => Augmented(route.tapErrorZIO(f), aspect) + case handled @ Handled(_, _, _) => handled + case Unhandled(rpm, handler, zippable, location) => Unhandled(rpm, handler.tapErrorZIO(f), zippable, location) + } + + /** + * Effectfully peeks at the unhandled failure cause of this Route. + */ + final def tapErrorCauseZIO[Err1 >: Err]( + f: Cause[Err] => ZIO[Any, Err1, Any], + )(implicit trace: Trace, ev: CheckResponse[Err]): Route[Env, Err1] = + self match { + case Provided(route, env) => + Provided(route.tapErrorCauseZIO(f), env) + case Augmented(route, aspect) => + Augmented(route.tapErrorCauseZIO(f), aspect) + case Handled(routePattern, handler, location) => + Handled( + routePattern, + handler.map(_.tapErrorCauseZIO { cause0 => + f(cause0.asInstanceOf[Cause[Nothing]]).catchAllCause(cause => ZIO.fail(Response.fromCause(cause))) + }), + location, + ) + case Unhandled(rpm, handler, zippable, location) => + Unhandled(rpm, handler.tapErrorCauseZIO(f), zippable, location) + } + /** * Allows the transformation of the Err type through a function allowing one * to build up a Routes in Stages targets the Unhandled case @@ -489,4 +527,16 @@ object Route { } } + sealed trait CheckResponse[-A] { def isResponse: Boolean } + object CheckResponse { + implicit val response: CheckResponse[Response] = new CheckResponse[Response] { + val isResponse = true + } + + // to avoid unnecessary allocation + private val otherInstance: CheckResponse[Nothing] = new CheckResponse[Nothing] { + val isResponse = false + } + implicit def other[A]: CheckResponse[A] = otherInstance.asInstanceOf[CheckResponse[A]] + } } diff --git a/zio-http/shared/src/main/scala/zio/http/Routes.scala b/zio-http/shared/src/main/scala/zio/http/Routes.scala index 84136a6595..3d77aa8ba0 100644 --- a/zio-http/shared/src/main/scala/zio/http/Routes.scala +++ b/zio-http/shared/src/main/scala/zio/http/Routes.scala @@ -102,6 +102,18 @@ final case class Routes[-Env, +Err](routes: Chunk[zio.http.Route[Env, Err]]) { s def handleErrorCauseZIO(f: Cause[Err] => ZIO[Any, Nothing, Response])(implicit trace: Trace): Routes[Env, Nothing] = new Routes(routes.map(_.handleErrorCauseZIO(f))) + /** + * Effectfully peeks at the unhandled failure of this Routes. + */ + def tapErrorZIO[Err1 >: Err](f: Err => ZIO[Any, Err1, Any])(implicit trace: Trace): Routes[Env, Err1] = + new Routes(routes.map(_.tapErrorZIO(f))) + + /** + * Effectfully peeks at the unhandled failure cause of this Routes. + */ + def tapErrorCauseZIO[Err1 >: Err](f: Cause[Err] => ZIO[Any, Err1, Any])(implicit trace: Trace): Routes[Env, Err1] = + new Routes(routes.map(_.tapErrorCauseZIO(f))) + /** * Allows the transformation of the Err type through an Effectful program * allowing one to build up Routes in Stages delegates to the Route.