Reputation: 36491
In an API I'm writing, I want to take and return JSON, even in cases of errors. I'm trying to figure out how to preserve all of the default
RejectionHandler
behavior, but convert the status codes and text into a JSON object. As the default behavior is specified within function calls, rather than as a data structure, it seems that the only way to do this is to convert the HttpEntity
of the result it produces. Is there a simple way to do this?
Upvotes: 2
Views: 1960
Reputation: 30095
You can write something like this in your HttpService
private val defaultRejectionHandler = RejectionHandler.default
implicit def myRejectionHandler =
RejectionHandler.newBuilder()
.handleAll[Rejection] { rejections ⇒
def prefixEntity(entity: ResponseEntity): ResponseEntity = entity match {
case HttpEntity.Strict(contentType, data) => {
import spray.json._
val text = ErrorResponse(0, "Rejection", data.utf8String).toJson.prettyPrint
HttpEntity(ContentTypes.`application/json`, text)
}
case _ =>
throw new IllegalStateException("Unexpected entity type")
}
mapResponseEntity(prefixEntity) {
defaultRejectionHandler(rejections).getOrElse {
complete(StatusCodes.InternalServerError)
}
}
}.handleNotFound {
complete(StatusCodes.Forbidden -> ErrorResponse(StatusCodes.NotFound.intValue, "NotFound", "Requested resource is not found"))
}.result()
where ErrorResponse
is possibly
case class ErrorResponse(error: ErrorInfo)
case class ErrorInfo(code: Int, `type`: String, message: String)
for which you can defined json marshallers.
Upvotes: 3
Reputation: 36491
I had my pieced together my own version, but it had some rough edges I didn't like. ruslan's answer gave me some ideas for improvement. Here's what I came up with, synthesizing the best of both approaches:
/**
* Modifies the Akka-Http default rejection handler to wrap the default
* message in JSON wrapper, preserving the original status code.
*
* @param rejectionWrapper wraps the message in a structure to format the
* resulting JSON object
* @param writer writer for the wrapper type
* @tparam WrapperType type of the wrapper
* @return the modified rejection handler
*/
def defaultRejectionHandlerAsJson[WrapperType](rejectionWrapper: String => WrapperType)(implicit writer: JsonWriter[WrapperType]) = {
def rejectionModifier(originalMessage: String): String = {
writer.write(rejectionWrapper(originalMessage)).prettyPrint
}
modifiedDefaultRejectionHandler(rejectionModifier, ContentTypes.`application/json`)
}
/**
* Modifies the Akka-Http default rejection handler, converting the default
* message to some other textual representation.
*
* @param rejectionModifier the modifier function
* @param newContentType the new Content Type, defaulting to text/plain
* UTF-8
* @return the modified rejection handler
*/
def modifiedDefaultRejectionHandler(rejectionModifier: String => String, newContentType: ContentType.NonBinary = ContentTypes.`text/plain(UTF-8)`) = new RejectionHandler {
def repackageRouteResult(entity: ResponseEntity): ResponseEntity = entity match {
// If the entity isn't Strict (and it definitely will be), don't bother
// converting, just throw an error, because something's weird.
case strictEntity: HttpEntity.Strict =>
val modifiedMessage = rejectionModifier(strictEntity.data.utf8String)
HttpEntity(newContentType, modifiedMessage)
case other =>
throw new Exception("Unexpected entity type")
}
def apply(v1: Seq[Rejection]): Option[Route] = {
// The default rejection handler should handle all possible rejections,
// so if this isn't the case, return a 503.
val originalResult = RejectionHandler.default(v1).getOrElse(complete(StatusCodes.InternalServerError))
Some(mapResponseEntity(repackageRouteResult) {
originalResult
})
}
}
Upvotes: 2