3
votes

In a REST API implemented with Play Framework (2.4), I'm using Action(parse.json) to parse JSON from incoming POST request body.

With my current code (see below),

  • Posting valid JSON with missing fields (e.g. {"foo": ""}) produces 400 with the response body {"error":"Missing input fields"}. This is fine and expected. ????

  • Posting completely invalid JSON (such as {,,,} or {\00}) produces 400 with a long HTML response body. ???? This comes from somewhere within parse.json.

In the latter case, how to get rid of the HTML response body? I'd like the response body to either contain a short JSON error message (such as {"error":"Invalid JSON input"}), or nothing at all. Does Play have a config option for this, or would I need to create a custom Action? What is the simplest way?

Controller method:

def test = Action(parse.json) { request =>
  request.body.validate[Input].map(i => {
    Ok(i.foo)
  }).getOrElse(BadRequest(errorJson("Missing input fields")))
}

Other stuff used above:

case class Input(foo: String, bar: String)

object Input {
  implicit val reads = Json.reads[Input]
}

case class ErrorJson(error: String)

object ErrorJson {
  implicit val writes = Json.writes[ErrorJson]
}

private def errorJson(message: String) = Json.toJson(ErrorJson(message))
2
Have you tried to override onError interceptor method on Global and catch the exception thrown to customize the error message? - Richeek
There is no Global in my app; it's deprecated in recent Play versions. Instead, providing custom HttpErrorHandler is the way to go. - Jonik
Ah I see. I use play 2.4 - Richeek
Yes, me too. In 2.4 (and 2.5 afaik) it is still possible to use many old APIs, which nonetheless are deprecated / no longer recommended; see Play 2.4 Migration Guide. - Jonik
official example: github.com/playframework/play-scala-rest-api-example/blob/2.6.x/… Im feeling that Play is not Rest API friendly. - WeiChing 林煒清

2 Answers

3
votes

The long html is produced by the default HttpErrorHandler. You can provide your own by following this guide. Quoting the example code:

class ErrorHandler extends HttpErrorHandler {

  def onClientError(request: RequestHeader, statusCode: Int, message: String) = {
    Future.successful(
      Status(statusCode)("A client error occurred: " + message)
    )
  }

  def onServerError(request: RequestHeader, exception: Throwable) = {
    Future.successful(
      InternalServerError("A server error occurred: " + exception.getMessage)
    )
  }
}

Note: if you manage your dependencies without Guice, you will have to provide your custom HttpErrorHandler in the ApplicationLoader

2
votes

Short answer: in https://github.com/playframework/playframework/blob/48a76b851946261a952a2edcc4b8dbeeb303e07b/framework/src/play/src/main/scala/play/api/mvc/ContentTypes.scala#L379 there is

Json.parse(bytes.iterator.asInputStream)

Json.parse delegates to Jacksons' parseJsValue, which throws exception. I cannot follow the code properly, why the exception isn't caught properly inside Iteratee framework.

Most straightforward solution, would be to replicate the code in ContentTypes.scala catching the exception around Json.parse and properly turning it into Left value of BodyParser's Iteratee. Unfortunately Play doesn't expose building blocks, so lot of copy-paste is needed if you want to do it as in Play.

Alternatively you could do dumb Iteratee.fold and direct Json.parse on accumulated bytearray, that's not good; you probably want to check accept headers , use bytearray builder, and limit the max size of the input

val betterJson: BodyParser[JsValue] = BodyParser("better json") { _request =>
  import play.api.libs.iteratee.Execution.Implicits.defaultExecutionContext

  Iteratee.fold(new Array[Byte](0)) { (bytes: Array[Byte], acc: Array[Byte]) =>
    bytes ++ acc
  } map { bytes =>
    val res: Either[Result, JsValue] = Try(Json.parse(bytes)) match {
      case Success(v) => Right(v)
      case Failure(e) => Left(BadRequest("bad json"))
    }
    res
  }
}

Using betterJson in a controller:

def test = Action(betterJson) { request =>
  request.body.validate[Int].map(i => {
    Ok(i.toString)
  }).getOrElse(BadRequest("my error"))
}

Tested with:

// works:
// $ curl -H "Content-Type: application/json" -X POST -d '123' localhost:9000/test
// 123
// $ curl -H "Content-Type: application/json" -X POST -d '{}' localhost:9000/test
// my error
//
// issue:
// $ curl -H "Content-Type: application/json" -X POST -d '.' localhost:9000/test
// bad json