ScalaのREST APIフレームワークSprayですが、エラーメッセージはデフォルトでtext/plain
になっています。
HTTP/1.1 400 Bad Request
Content-Length: 40
Content-Type: text/plain; charset=UTF-8
Date: Wed, 03 Jun 2015 05:51:49 GMT
Request entity expected but not supplied
REST APIを実装する上で、エラーメッセージはJSONにしたいことがあります。
Sprayのエラーハンドリング
Sprayは各directiveごとにリクエストを受け入れるか拒否(reject)するかの判断が加わります。拒否された場合、ルーティングからは外れ、RejectionHandlerがレスポンスの役目を担います。
デフォルトのRejectionHandlerがtext/plain
のレスポンスを作っているので、これを換装することでカスタムのエラーレスポンスを作ることができます。
RejectionHandlerの自作
例えば、エラーメッセージとして次のようなJSONを返えす実装をしてみたいと思います。
HTTP/1.1 422 Unprocessable Entity
Content-Length: 140
Content-Type: application/json; charset=UTF-8
Date: Wed, 03 Jun 2015 05:56:57 GMT
{
"code": "validation_failed",
"errors": [
{
"code": "missing_field",
"field": "user_id"
}
],
"message": "Validation failed"
}
まず、エラーメッセージ用のcase classを定義します。
case class Rejection(code: String, message: String, errors: Option[Seq[FieldRejection]])
case class FieldRejection(field: String, code: String)
次に、このcase classをJSONにシリアライズできるよう、protocolを作成します。
object RejectionJsonProtocol extends DefaultJsonProtocol with SprayJsonSupport {
implicit val FieldRejectionFormat = jsonFormat2(FieldRejection)
implicit val RejectionFormat = jsonFormat3(Rejection)
}
最後に、RejectionHandler
を継承して自前のハンドラを作ります。
trait JsonRejectionHandler {
import RejectionJsonProtocol._
implicit val jsonRejectionHandler = RejectionHandler {
case MalformedRequestContentRejection(_, _) :: _ =>
complete(UnprocessableEntity, Rejection("validation_failed", msg, None))
}
}
MalformedRequestContentRejection
は予め定義されたRejectionの1つです。エラーをJSONにしたいRejectionのぶんだけ、ここにcase
を定義して拡張していきます。
以下が全体的なコードです。
package shouldbee.spray
import spray.httpx.SprayJsonSupport
import spray.routing._
import spray.http.StatusCodes._
import spray.routing.directives.RouteDirectives._
import spray.json.{ DeserializationException, DefaultJsonProtocol }
case class Rejection(code: String, message: String, errors: Option[Seq[FieldRejection]])
case class FieldRejection(field: String, code: String)
object RejectionJsonProtocol extends DefaultJsonProtocol with SprayJsonSupport {
implicit val FieldRejectionFormat = jsonFormat2(FieldRejection)
implicit val RejectionFormat = jsonFormat3(Rejection)
}
trait JsonRejectionHandler {
import RejectionJsonProtocol._
implicit val jsonRejectionHandler = RejectionHandler {
case MalformedRequestContentRejection(msg, cause) :: _ =>
cause match {
case Some(DeserializationException(_, prev, fieldNames)) =>
val code = prev match {
case e: NoSuchElementException => "missing_field"
case e: IllegalArgumentException => "malformed"
case e: Throwable => e.getClass.getName
}
val errors: Seq[FieldRejection] = fieldNames.map(FieldRejection(_, code))
complete(UnprocessableEntity, Rejection("validation_failed", "Validation failed", Some(errors)))
case what @ _ =>
complete(UnprocessableEntity, Rejection("validation_failed", msg, None))
}
}
}
Serviceに組み込む
最後に、自作したRejectionHandlerをServiceにmixinすることで、エラーがJSONになります。
trait YourAppService extends HttpService with JsonRejectionHandler {
...
}