Rejection in Akka HTTP: Handle Errors Properly
Learn more about rejection in Akka HTTP and how it helps in handling error scenarios in your application.
Join the DZone community and get the full member experience.
Join For FreeIn this blog, we will demonstrate rejection in the Akka HTTP and how it helps in handling error scenarios in our application. Let's get started.
The concept of rejection in Akka HTTP helps to properly deal with error scenarios. For example, with a filtering directive, like the get directive, it does not allow the request to pass through its inner route. As the filter condition is not satisfied, a rejection concept is invoked.
The ~
(Tilde) operator is also called the chaining operator. As it is used to connect two or more paths, the Request allows flowing through the routing structure. And try to find another route that can complete it. If a request does not find any route, it will generate a rejection.
By default, a response for rejected requests was generated by the “handle not found” method of RejectionHandler
.
xxxxxxxxxx
trait RejectionHandler extends (immutable.Seq[Rejection] ⇒Option[Route])
We will understand this in detail through the following example:
xxxxxxxxxx
val route: Route =
path("hello") {
get {
complete(HttpResponse(entity = "Hello world"))
} } ~
path("ping") {
get {
complete(HttpResponse(entity = "PONG!")) }
}
If we access the path with “hello,” then we will get the response “Hello world.” At the same time, we will get a response “PONG!” for the route “ping.”
You may have questions like: "What happens if we hit any other route?" We’ll get the following response: “The requested resource could not be found.”
If we want this response in a more descriptive way, we can customize the RejectionHandler
. The easiest way to construct a RejectionHandler
is with RejectionHandler.newBuilder()
that Akka HTTP provides.
Refer to following code snippet:
xxxxxxxxxx
implicit def rejectionHandler = RejectionHandler.newBuilder()
.handleNotFound {
complete(HttpResponse(NotFound, entity = HttpEntity(ContentTypes.`application/json`, Json.stringify(
Json.toJson(
ErrorResponse(
NotFound.intValue, "NotFound", "The requested resource could not be found.")
)
)
)))
}
We’ll get the response as follows:
xxxxxxxxxx
{
"code": 404,
"message": "NotFound",
"reason": "The requested resource could not be found."
}
Where ErrorResponse
is the case class:
case class ErrorResponse(code: Int, message: String, reason: String)
There are three helper methods in RejectionHandler
:
1. handleNotFound(Route)
:
As described in the above example, “Resource Not Found” is special. As it is represented with an empty rejection set, the handle not found helper lets you specify the “recovery route” for this case.
2. handle(PartialFunction[Rejection, Route])
:
Handles provided the type of rejection with given partial function. This partial function produces a route, which is run when certain rejection occurs.
3. handleAll[T <: Rejection: ClassTag](f: immutable.Seq[T] => Route)
:
Handles all rejections of a certain type at the same time. This is useful for cases where you need access to more than the first rejection of a certain type, e. g. for producing the error message to an unsupported request method.
There are many predefined rejections that are provided by Akka HTTP. They are MethodRejection
, AuthorizationFailedRejection
, MissingCookieRejection
, and MissingQueryParamRejection
.
We invoked these handle calls through implicit definition using “newBuilder()
” to build a new RejectionHandler
. We can tell this handler how to handle a particular rejection.
1. MethodRejection
:
This rejection occurs when a user uses an unsupported method to access request. In the following example, a supported method is get
. This will generate a rejection when we try to access using post
, put
, or any other method.
xxxxxxxxxx
val route: Route =
path("hello") {
get {
complete(HttpResponse(OK, entity = HttpEntity(ContentTypes.`application/json`, Json.stringify(
Json.toJson(
ProperResponse(
OK.intValue,"Hello world")))
)))}
}
implicit def rejectionHandler = RejectionHandler.newBuilder()
.handleAll[MethodRejection] { methodRejections =>
val names = methodRejections.map(_.supported.name)
complete(HttpResponse(MethodNotAllowed, entity = HttpEntity(ContentTypes.`application/json`, Json.stringify(
Json.toJson(
ErrorResponse(
MethodNotAllowed.intValue, "Method Rejection", s"Method not supported! Supported for : ${names mkString "or"}!"))
)
)))
}
For unsupported method, the output will be:
xxxxxxxxxx
{
"code": 405,
"message": "Method Rejection",
"reason": "Method not supported! Supported for : GET!"
}
2. MissingQueryParamRejection
:
MissingQueryParamRejection
arises when the user doesn't mention the required parameters in Request
.
In the given example, if we hit URL, localhost:8080/paint?color = red.
In this scenario, the Response would be:
Request has missing required query parameter for = ‘bgColor’.
And if we hit URL, we get “localhost:8080/paint?bgColor = red." Then, Response will be: Request has missing required query parameter for = ‘color’.
When we mention both parameters in the URL localhost:8080/paint?bgColor=red&color=blue, then we get the proper result:
xxxxxxxxxx
{
"code": 200,
"message": "You mention color is blue and background color is red"
}
3. AuthorizationFailedRejection
:
This Rejection is created by the “authorize” directive. This signals that the request was rejected because the user is not authorized.
Let’s see an example:
xxxxxxxxxx
val route: Route =
path("login") {
parameters('username, 'password) {
(username, password) =>
if (username.equals("knoldus") && password.equals("pune")) {
complete(HttpResponse(NotFound,entity = HttpEntity(ContentTypes.`application/json`, Json.stringify(
Json.toJson(
ProperResponse(
OK.intValue, "Login Successful")))
)))}
else {
reject(AuthorizationFailedRejection)
} } }
implicit def rejectionHandler =
RejectionHandler.newBuilder()
.handle { case AuthorizationFailedRejection =>
complete(HttpResponse(BadRequest, entity = HttpEntity(ContentTypes.`application/json`, Json.stringify(
Json.toJson(
ErrorResponse(
BadRequest.intValue, "AuthorizationFailedRejection", "You have entered invalid credentials")
)
)
)))
}
If we hit the path with the right credential values, such as “localhost:8080/login?username = knoldus&password = pune,” we will get a response as:
xxxxxxxxxx
{
"code": 200,
"message": "Login Successful"
}
When we try to provide access with the wrong values, like “ localhost:8080/login?username = software&password = abcd, ” we will get a response as:
xxxxxxxxxx
{
"code": 400,
"message": "AuthorizationFailedRejection",
"reason": "You have entered invalid credentials"
}
In conclusion, this is how RejectionHandler
can be customized to handle errors in code. You can view the entire code on GitHub.
Thanks for reading! Happy coding!
This article was originally published on the Knoldus blog.
Further Reading
Published at DZone with permission of Praful Bangar. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments