diff --git a/zio-http/src/main/scala/zio/http/Header.scala b/zio-http/src/main/scala/zio/http/Header.scala index 1e3a594210..5be397f714 100644 --- a/zio-http/src/main/scala/zio/http/Header.scala +++ b/zio-http/src/main/scala/zio/http/Header.scala @@ -4173,8 +4173,9 @@ object Header { 2xx warn-codes describe some aspect of the representation that is not rectified by a validation and will not be deleted by a cache after validation unless a full response is sent. */ - val warnCode: Int = Try { - Integer.parseInt(warningString.split(" ")(0)) + val warnCodeString = warningString.split(" ")(0) + val warnCode: Int = Try { + Integer.parseInt(warnCodeString) }.getOrElse(-1) /* @@ -4187,11 +4188,11 @@ object Header { An advisory text describing the error. */ - val descriptionStartIndex = warningString.indexOf('\"') - val descriptionEndIndex = warningString.indexOf("\"", warningString.indexOf("\"") + 1) + val descriptionStartIndex = warningString.indexOf('\"', warnCodeString.length + warnAgent.length) + 1 + val descriptionEndIndex = warningString.indexOf("\"", descriptionStartIndex) val description = Try { - warningString.substring(descriptionStartIndex, descriptionEndIndex + 1) + warningString.substring(descriptionStartIndex, descriptionEndIndex) }.getOrElse("") /* @@ -4249,17 +4250,16 @@ object Header { def render(warning: Warning): String = warning match { - case Warning(code, agent, text, date) => { + case Warning(code, agent, text, date) => val formattedDate = date match { case Some(value) => DateEncoding.default.encodeDate(value) case None => "" } if (formattedDate.isEmpty) { - code.toString + " " + agent + " " + text + code.toString + " " + agent + " " + '"' + text + '"' } else { - code.toString + " " + agent + " " + text + " " + '"' + formattedDate + '"' + code.toString + " " + agent + " " + '"' + text + '"' + " " + '"' + formattedDate + '"' } - } } } diff --git a/zio-http/src/main/scala/zio/http/Response.scala b/zio-http/src/main/scala/zio/http/Response.scala index 4a630f4d54..d051756f62 100644 --- a/zio-http/src/main/scala/zio/http/Response.scala +++ b/zio-http/src/main/scala/zio/http/Response.scala @@ -136,7 +136,7 @@ object Response { val message2 = OutputEncoder.encodeHtml(if (message == null) status.text else message) - Response(status = status, headers = Headers(Header.Warning(status.code, "ZIO HTTP", message2))) + Response(status = status, headers = Headers(Header.Warning(199, "ZIO HTTP", message2))) } def error(status: Status.Error): Response = diff --git a/zio-http/src/main/scala/zio/http/Route.scala b/zio-http/src/main/scala/zio/http/Route.scala index c51d6fc207..ec1994e453 100644 --- a/zio-http/src/main/scala/zio/http/Route.scala +++ b/zio-http/src/main/scala/zio/http/Route.scala @@ -16,9 +16,6 @@ package zio.http import zio._ -import zio.stacktracer.TracingImplicits.disableAutoTrace - -import zio.http.Route.Provided /* * Represents a single route, which has either handled its errors by converting @@ -47,14 +44,16 @@ sealed trait Route[-Env, +Err] { self => def asErrorType[Err2](implicit ev: Err <:< Err2): Route[Env, Err2] = self.asInstanceOf[Route[Env, Err2]] /** - * Handles the error of the route. This method can be used to convert a route - * that does not handle its errors into one that does handle its errors. + * Handles all typed errors in the route by converting them into responses. + * This method can be used to convert a route that does not handle its errors + * into one that does handle its errors. */ final def handleError(f: Err => Response)(implicit trace: Trace): Route[Env, Nothing] = self.handleErrorCause(Response.fromCauseWith(_)(f)) /** - * Handles the error of the route. This method can be used to convert a route + * Handles all typed errors, as well as all non-recoverable errors, by + * converting them into responses. This method can be used to convert a route * that does not handle its errors into one that does handle its errors. */ final def handleErrorCause(f: Cause[Err] => Response)(implicit trace: Trace): Route[Env, Nothing] = @@ -83,6 +82,12 @@ sealed trait Route[-Env, +Err] { self => Handled(rpm.routePattern, handler2, location) } + /** + * Handles all typed errors, as well as all non-recoverable errors, by + * converting them into a ZIO effect that produces the response. This method + * can be used to convert a route that does not handle its errors into one + * that does handle its errors. + */ final def handleErrorCauseZIO( f: Cause[Err] => ZIO[Any, Nothing, Response], )(implicit trace: Trace): Route[Env, Nothing] = @@ -109,6 +114,88 @@ sealed trait Route[-Env, +Err] { self => Handled(rpm.routePattern, handler2, location) } + /** + * Handles all typed errors in the route by converting them into responses, + * taking into account the request that caused the error. This method can be + * used to convert a route that does not handle its errors into one that does + * handle its errors. + */ + final def handleErrorRequest(f: (Err, Request) => Response)(implicit trace: Trace): Route[Env, Nothing] = + self.handleErrorRequestCause((request, cause) => Response.fromCauseWith(cause)(f(_, request))) + + /** + * Handles all typed errors, as well as all non-recoverable errors, by + * converting them into responses, taking into account the request that caused + * the error. This method can be used to convert a route that does not handle + * its errors into one that does handle its errors. + */ + final def handleErrorRequestCause(f: (Request, Cause[Err]) => Response)(implicit trace: Trace): Route[Env, Nothing] = + self match { + case Provided(route, env) => Provided(route.handleErrorRequestCause(f), env) + case Augmented(route, aspect) => Augmented(route.handleErrorRequestCause(f), aspect) + case Handled(routePattern, handler, location) => Handled(routePattern, handler, location) + + case Unhandled(rpm, handler, zippable, location) => + val handler2: Handler[Env, Response, Request, Response] = { + val paramHandler = + Handler.fromFunctionZIO[(rpm.Context, Request)] { case (ctx, request) => + rpm.routePattern.decode(request.method, request.path) match { + case Left(error) => ZIO.dieMessage(error) + case Right(value) => + val params = rpm.zippable.zip(value, ctx) + + handler(zippable.zip(params, request)) + } + } + + // Sandbox before applying aspect: + rpm.aspect.applyHandlerContext( + Handler.fromFunctionHandler[(rpm.Context, Request)] { case (_, req) => + paramHandler.mapErrorCause(f(req, _)) + }, + ) + } + + Handled(rpm.routePattern, handler2, location) + } + + /** + * Handles all typed errors, as well as all non-recoverable errors, by + * converting them into a ZIO effect that produces the response, taking into + * account the request that caused the error. This method can be used to + * convert a route that does not handle its errors into one that does handle + * its errors. + */ + final def handleErrorRequestCauseZIO( + f: (Request, Cause[Err]) => ZIO[Any, Nothing, Response], + )(implicit trace: Trace): Route[Env, Nothing] = + self match { + case Provided(route, env) => Provided(route.handleErrorRequestCauseZIO(f), env) + case Augmented(route, aspect) => Augmented(route.handleErrorRequestCauseZIO(f), aspect) + case Handled(routePattern, handler, location) => Handled(routePattern, handler, location) + + case Unhandled(rpm, handler, zippable, location) => + val handler2: Handler[Env, Response, Request, Response] = { + val paramHandler = + Handler.fromFunctionZIO[(rpm.Context, Request)] { case (ctx, request) => + rpm.routePattern.decode(request.method, request.path) match { + case Left(error) => ZIO.dieMessage(error) + case Right(value) => + val params = rpm.zippable.zip(value, ctx) + + handler(zippable.zip(params, request)) + } + } + rpm.aspect.applyHandlerContext( + Handler.fromFunctionHandler[(rpm.Context, Request)] { case (_, req) => + paramHandler.mapErrorCauseZIO(f(req, _)) + }, + ) + } + + Handled(rpm.routePattern, handler2, location) + } + /** * Determines if the route is defined for the specified request. */ diff --git a/zio-http/src/main/scala/zio/http/Routes.scala b/zio-http/src/main/scala/zio/http/Routes.scala index 3fb19c5781..035bcfb4c6 100644 --- a/zio-http/src/main/scala/zio/http/Routes.scala +++ b/zio-http/src/main/scala/zio/http/Routes.scala @@ -16,7 +16,6 @@ package zio.http import zio._ -import zio.stacktracer.TracingImplicits.disableAutoTrace /** * Represents a collection of routes, each of which is defined by a pattern and @@ -65,20 +64,59 @@ final class Routes[-Env, +Err] private (val routes: Chunk[zio.http.Route[Env, Er /** * Handles all typed errors in the routes by converting them into responses. + * This method can be used to convert routes that do not handle their errors + * into ones that do handle their errors. */ def handleError(f: Err => Response)(implicit trace: Trace): Routes[Env, Nothing] = new Routes(routes.map(_.handleError(f))) /** * Handles all typed errors, as well as all non-recoverable errors, by - * converting them into responses. + * converting them into responses. This method can be used to convert routes + * that do not handle their errors into ones that do handle their errors. */ def handleErrorCause(f: Cause[Err] => Response)(implicit trace: Trace): Routes[Env, Nothing] = new Routes(routes.map(_.handleErrorCause(f))) + /** + * Handles all typed errors, as well as all non-recoverable errors, by + * converting them into a ZIO effect that produces the response. This method + * can be used to convert routes that do not handle their errors into ones + * that do handle their errors. + */ def handleErrorCauseZIO(f: Cause[Err] => ZIO[Any, Nothing, Response])(implicit trace: Trace): Routes[Env, Nothing] = new Routes(routes.map(_.handleErrorCauseZIO(f))) + /** + * Handles all typed errors in the routes by converting them into responses, + * taking into account the request that caused the error. This method can be + * used to convert routes that do not handle their errors into ones that do + * handle their errors. + */ + def handleErrorRequest(f: (Err, Request) => Response)(implicit trace: Trace): Routes[Env, Nothing] = + new Routes(routes.map(_.handleErrorRequest(f))) + + /** + * Handles all typed errors in the routes by converting them into responses, + * taking into account the request that caused the error. This method can be + * used to convert routes that do not handle their errors into ones that do + * handle their errors. + */ + def handleErrorRequestCause(f: (Request, Cause[Err]) => Response)(implicit trace: Trace): Routes[Env, Nothing] = + new Routes(routes.map(_.handleErrorRequestCause(f))) + + /** + * Handles all typed errors, as well as all non-recoverable errors, by + * converting them into a ZIO effect that produces the response, taking into + * account the request that caused the error. This method can be used to + * convert routes that do not handle their errors into ones that do handle + * their errors. + */ + def handleErrorRequestCauseZIO(f: (Request, Cause[Err]) => ZIO[Any, Nothing, Response])(implicit + trace: Trace, + ): Routes[Env, Nothing] = + new Routes(routes.map(_.handleErrorRequestCauseZIO(f))) + /** * Returns new routes that have each been provided the specified environment, * thus eliminating their requirement for any specific environment. diff --git a/zio-http/src/test/scala/zio/http/RouteSpec.scala b/zio-http/src/test/scala/zio/http/RouteSpec.scala index 13b4baf74a..8a1aa26a09 100644 --- a/zio-http/src/test/scala/zio/http/RouteSpec.scala +++ b/zio-http/src/test/scala/zio/http/RouteSpec.scala @@ -64,7 +64,7 @@ object RouteSpec extends ZIOHttpSpec { ), suite("error handle")( test("handleErrorCauseZIO should execute a ZIO effect") { - val route = Method.GET / "endpoint" -> handler { (req: Request) => ZIO.fail(new Exception("hmm...")) } + val route = Method.GET / "endpoint" -> handler { (_: Request) => ZIO.fail(new Exception("hmm...")) } for { p <- zio.Promise.make[Exception, String] @@ -77,6 +77,43 @@ object RouteSpec extends ZIOHttpSpec { } yield assertTrue(extractStatus(response) == Status.InternalServerError, result.contains("hmm...")) }, + test("handleErrorCauseRequestZIO should produce an error based on the request") { + val route = Method.GET / "endpoint" -> handler { (_: Request) => ZIO.fail(new Exception("hmm...")) } + for { + p <- zio.Promise.make[Exception, String] + + errorHandled = route + .handleErrorRequestCauseZIO((req, c) => + p.failCause(c).as(Response.internalServerError(s"error accessing ${req.path.encode}")), + ) + + request = Request.get(URL.decode("/endpoint").toOption.get) + response <- errorHandled.toHttpApp.runZIO(request) + result <- p.await.catchAllCause(c => ZIO.succeed(c.prettyPrint)) + resultWarning <- ZIO.fromOption(response.headers.get(Header.Warning).map(_.text)) + + } yield assertTrue( + extractStatus(response) == Status.InternalServerError, + resultWarning == "error accessing /endpoint", + result.contains("hmm..."), + ) + }, + test("handleErrorCauseRequest should produce an error based on the request") { + val route = Method.GET / "endpoint" -> handler { (_: Request) => ZIO.fail(new Exception("hmm...")) } + val errorHandled = + route.handleErrorRequest((e, req) => + Response.internalServerError(s"error accessing ${req.path.encode}: ${e.getMessage}"), + ) + val request = Request.get(URL.decode("/endpoint").toOption.get) + for { + response <- errorHandled.toHttpApp.runZIO(request) + resultWarning <- ZIO.fromOption(response.headers.get(Header.Warning).map(_.text)) + + } yield assertTrue( + extractStatus(response) == Status.InternalServerError, + resultWarning == "error accessing /endpoint: hmm...", + ) + }, ), ) } diff --git a/zio-http/src/test/scala/zio/http/headers/WarningSpec.scala b/zio-http/src/test/scala/zio/http/headers/WarningSpec.scala index a74850dd7a..ba0caec502 100644 --- a/zio-http/src/test/scala/zio/http/headers/WarningSpec.scala +++ b/zio-http/src/test/scala/zio/http/headers/WarningSpec.scala @@ -58,11 +58,11 @@ object WarningSpec extends ZIOHttpSpec { }, test("Accepts Valid Warning with Date") { assertTrue( - Warning.parse(validWarningWithDate) == Right(Warning(112, "-", "\"cache down\"", Some(stubDate))), + Warning.parse(validWarningWithDate) == Right(Warning(112, "-", "cache down", Some(stubDate))), ) }, test("Accepts Valid Warning without Date") { - assertTrue(Warning.parse(validWarning) == Right(Warning(110, "anderson/1.3.37", "\"Response is stale\""))) + assertTrue(Warning.parse(validWarning) == Right(Warning(110, "anderson/1.3.37", "Response is stale"))) }, test("parsing and encoding is symmetrical for warning with Date") { val encodedWarningwithDate = Warning.render(Warning.parse(validWarningWithDate).toOption.get)