Skip to content

Commit

Permalink
Error handling methods on Route/Routes that can access Request
Browse files Browse the repository at this point in the history
Bug fix on Warning header
  • Loading branch information
987Nabil committed Dec 7, 2023
1 parent 58e0594 commit d6dbc78
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 22 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ZIO HTTP is a scala library for building http apps. It is powered by ZIO and [Ne
Setup via `build.sbt`:

```scala
libraryDependencies += "dev.zio" %% "zio-http" % "3.0.0-RC3"
libraryDependencies += "dev.zio" %% "zio-http" % "3.0.0-RC4"
```

**NOTES ON VERSIONING:**
Expand Down
18 changes: 9 additions & 9 deletions zio-http/src/main/scala/zio/http/Header.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

/*
Expand All @@ -4187,11 +4188,11 @@ object Header {
<warn-text>
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("")

/*
Expand Down Expand Up @@ -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 + '"'
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion zio-http/src/main/scala/zio/http/Response.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
99 changes: 93 additions & 6 deletions zio-http/src/main/scala/zio/http/Route.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,14 +44,25 @@ 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 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.handleErrorCauseRequest((cause, request) => Response.fromCauseWith(cause)(f(_, request)))

/**
* 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] =
Expand Down Expand Up @@ -83,6 +91,48 @@ 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 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 handleErrorCauseRequest(f: (Cause[Err], Request) => Response)(implicit trace: Trace): Route[Env, Nothing] =
self match {
case Provided(route, env) => Provided(route.handleErrorCauseRequest(f), env)
case Augmented(route, aspect) => Augmented(route.handleErrorCauseRequest(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. 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] =
Expand All @@ -109,6 +159,43 @@ 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, 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 handleErrorCauseRequestZIO(
f: (Cause[Err], Request) => ZIO[Any, Nothing, Response],
)(implicit trace: Trace): Route[Env, Nothing] =
self match {
case Provided(route, env) => Provided(route.handleErrorCauseRequestZIO(f), env)
case Augmented(route, aspect) => Augmented(route.handleErrorCauseRequestZIO(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.
*/
Expand Down
42 changes: 40 additions & 2 deletions zio-http/src/main/scala/zio/http/Routes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,17 +64,56 @@ 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 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, 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 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 handleErrorCauseRequest(f: (Cause[Err], Request) => Response)(implicit trace: Trace): Routes[Env, Nothing] =
new Routes(routes.map(_.handleErrorCauseRequest(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 handleErrorCauseRequestZIO(f: (Cause[Err], Request) => ZIO[Any, Nothing, Response])(implicit
trace: Trace,
): Routes[Env, Nothing] =
new Routes(routes.map(_.handleErrorCauseRequestZIO(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)))

Expand Down
39 changes: 38 additions & 1 deletion zio-http/src/test/scala/zio/http/RouteSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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
.handleErrorCauseRequestZIO((c, req) =>
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...",
)
},
),
)
}
4 changes: 2 additions & 2 deletions zio-http/src/test/scala/zio/http/headers/WarningSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit d6dbc78

Please sign in to comment.