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 4d195e2f1..e9e6fa9be 100644 --- a/zio-http/jvm/src/test/scala/zio/http/RouteSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/RouteSpec.scala @@ -203,6 +203,58 @@ object RouteSpec extends ZIOHttpSpec { refValue <- ref.get } yield assertTrue(extractStatus(response) == Status.Ok, !refValue) }, + 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) + }, test( "Routes with context can eliminate environment type partially when elimination produces intersection type environment", ) { 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 617a0f358..519eda4ac 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 /* @@ -158,6 +160,42 @@ sealed trait Route[-Env, +Err] { self => Handled(pattern, 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 @@ -464,4 +502,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 5847d9524..f94fcc304 100644 --- a/zio-http/shared/src/main/scala/zio/http/Routes.scala +++ b/zio-http/shared/src/main/scala/zio/http/Routes.scala @@ -104,6 +104,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.