acjay
acjay

Reputation: 36491

Return JSON errors in akka-http API

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

Answers (2)

expert
expert

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

acjay
acjay

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

Related Questions