Search code examples
scalaplayframeworkplayframework-2.4

play2: FakeRequest().withBody(body) automatically converted to Request[AnyContentAsEmpty] in controller


I'm working on a play-2.4 project, and wrote a controller like:

package controllers

import play.api._
import play.api.mvc._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

class Application extends Controller {
  def index = Action.async { implicit request =>
    Future { Ok(request.body.asJson.get) }
  }
}

with POST / controllers.Application.index in conf/routes.

I checked this worked fine by executing curl --request POST --header "Content-type: application/json" --data '{"foo":"bar"}' http://localhost:9000/.

Now I wrote a spec for this controller:

package controllers

import org.specs2.mutable._
import org.specs2.runner._
import org.junit.runner._

import play.api.test._
import play.api.test.Helpers._

@RunWith(classOf[JUnitRunner])
class ApplicationSpec extends Specification {
  "Application" should {
    val controller = new Application
    val fakeJson = """{ "foo":"bar" }"""
    val fakeRequest = FakeRequest()
      .withHeaders("Content-type" -> "application/json")
      .withBody(fakeJson)
    val index = controller.index()(fakeRequest).run
    status(index) must equalTo(OK)
  }
}

but this resulted in a runtime error:

[error]    None.get (Application.scala:11)
[error] controllers.Application$$anonfun$index$1$$anonfun$apply$1.apply(Application.scala:11)
[error] controllers.Application$$anonfun$index$1$$anonfun$apply$1.apply(Application.scala:11)

I inserted println(request.body) in the controller and found the request body was AnyContentAsEmpty, meaning fakeJson was removed from fakeRequest.

How can I attach a JSON to FakeRequest properly?

*note: Although I can write like FakeRequest(POST, '/', FakeHeaders(), fakeJson), but I think this is not good because controller spec should not handle HTTP methods or routes.

I'll be grateful for any help.


Solution

  • If a client makes an HTTP POST to your action with a request that isn't JSON, request.body.asJson.get will throw an exception.

    1. body.asJson has return type Option[JsValue] and it returns a None if the request wasn't JSON.
    2. Calling get on a None throws a java.util.NoSuchElementException.
    3. This exception manifests as Play returning a 500 Internal Server Error.

    You should replace def index = Action.async ... with an action that uses a JSON body parser instead:

    import play.api.mvc.BodyParsers.parse
    
    def index = Action.async(parse.json) ...
    

    This achieves a few things:

    1. It's more self-documenting (the action says "I expect JSON" right there in the method declaration).
    2. Play will generate a 400 Bad Request if the POST wasn't JSON. This is more appropriate than the 500 Internal Server Error caused by your None.get.
    3. It will make request.body a JsValue instead of AnyContent. So you can replace request.body.asJson.get with simply request.body. In general you should avoid calling Option.get because it's not safe and there's usually a better way to achieve what you want (using the appropriate body parser happens to be that better way in this case).

    Now this test no longer compiles, as opposed to throwing the exception caused by None.get:

    val fakeJson = """{ "foo":"bar" }"""
    val fakeRequest = FakeRequest()
      .withHeaders("Content-type" -> "application/json")
      .withBody(fakeJson)
    val index = controller.index()(fakeRequest)
    status(index) must equalTo(OK)
    

    Forcing you to replace it with the version from your answer:

    val fakeJson = play.api.libs.json.Json.parse("""{ "foo":"bar" }""") 
    val fakeRequest = FakeRequest().withBody(fakeJson)              
    val index = controller.index()(fakeRequest)                         
    status(index) must equalTo(OK)
    

    My final suggestion is that you use Json.obj to clean up your test:

    val fakeJson = Json.obj("foo" -> "bar")