Playframework: Unable to test simple Controller with JSON Body

Created on 3 Oct 2017  路  4Comments  路  Source: playframework/playframework

Are you looking for help?

No

Play Version

2.6.5

API

Scala

Operating System

MacOS:

Darwin ***.local 17.0.0 Darwin Kernel Version 17.0.0: Thu Aug 24 21:48:19 PDT 2017; root:xnu-4570.1.46~2/RELEASE_X86_64 x86_64

JDK (Oracle 1.8.0_72, OpenJDK 1.8.x, Azul Zing)

java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

Library Dependencies

scalaVersion := "2.12.3"
"org.scalatestplus.play" %% "scalatestplus-play" % "3.1.1"

Expected Behavior

  1. Status of tested controller method is returned

Actual Behavior

  1. 400 status is returned
  2. The following error message is returned in response body

Bad Request
For request 'GET /' [Invalid Json: No content to map due to end-of-input at [Source: akka.util.ByteIterator$ByteArrayIterator$$anon$1@161d95c6; line: 1, column: 0]]

Reproducible Test Case

Please, find the following repository to reproduce the issue:
https://github.com/Abrasha/playframework-issue-example

Most helpful comment

Hi @Abrasha, if you add type annotations into your test you can sort of see what's happening:

val request: Request[AnyContentAsJson] = FakeRequest()
  .withJsonBody(Json.toJson(person))
  .withHeaders(CONTENT_TYPE -> JSON)
val acceptAction: Action[JsValue] = homeController.accept
val response: Accumulator[ByteString, Result] = acceptAction.apply(request)
assert(status(response) === CREATED)

The problem is that acceptAction.apply is overloaded - you can either pass a RequestHeader or a Request[JsValue]. Since you're trying to pass a Request[AnyContentAsJson] this matches the RequestHeader version. This returns an Accumulator.

When you call homeController.accept.apply you don't get a result back, you get an Accumulator[ByteString, Result]. When you call status(response) this feeds an empty ByteString to the accumulator, resulting in a parsing error.

The fix is to make a Request[JsValue] which matches the type of acceptAction. If you use a Request[JsValue] it calls the correct accept method, returning a Future[Result]

val request: Request[AnyContentAsJson] = FakeRequest()
  .withBody(Json.toJson(person)) // <--- change here
  .withHeaders(CONTENT_TYPE -> JSON)
val acceptAction: Action[JsValue] = homeController.accept
val response: Future[Result] = acceptAction.apply(request) // <-- different type here
assert(status(response) === CREATED)

The alternative fix is to use an AnyContent BodyParser鈥攚hich is the default anyway. See https://www.playframework.com/documentation/2.6.x/ScalaTestingWithScalaTest#Unit-Testing-EssentialAction for an example how this works. You'll need to use request.body.asJson.get inside your action to get the result.

By the way, I realise this is very confusing but unfortunately it's too hard to change Play to make it less confusing. There are several design issues coming together here:

  • Request extends RequestHeader - this causes issues all over the place, but we can't change it now
  • Action overloads the apply method for everything instead of having methods with separate names, e.g. accumulator(rh: RequestHeader) and invoke(r: Request[T])
  • AnyContent should only be used rarely but it's the default in our code and docs
  • Action is also an ActionBuilder鈥攁gain using apply methods鈥攁nd making it confusing to understand how Actions are built

I'm going to close this ticket now because it works as designed and we unfortunately can't change the design!

All 4 comments

Thanks for the report. I'll take a look at this now.

Hi @Abrasha, if you add type annotations into your test you can sort of see what's happening:

val request: Request[AnyContentAsJson] = FakeRequest()
  .withJsonBody(Json.toJson(person))
  .withHeaders(CONTENT_TYPE -> JSON)
val acceptAction: Action[JsValue] = homeController.accept
val response: Accumulator[ByteString, Result] = acceptAction.apply(request)
assert(status(response) === CREATED)

The problem is that acceptAction.apply is overloaded - you can either pass a RequestHeader or a Request[JsValue]. Since you're trying to pass a Request[AnyContentAsJson] this matches the RequestHeader version. This returns an Accumulator.

When you call homeController.accept.apply you don't get a result back, you get an Accumulator[ByteString, Result]. When you call status(response) this feeds an empty ByteString to the accumulator, resulting in a parsing error.

The fix is to make a Request[JsValue] which matches the type of acceptAction. If you use a Request[JsValue] it calls the correct accept method, returning a Future[Result]

val request: Request[AnyContentAsJson] = FakeRequest()
  .withBody(Json.toJson(person)) // <--- change here
  .withHeaders(CONTENT_TYPE -> JSON)
val acceptAction: Action[JsValue] = homeController.accept
val response: Future[Result] = acceptAction.apply(request) // <-- different type here
assert(status(response) === CREATED)

The alternative fix is to use an AnyContent BodyParser鈥攚hich is the default anyway. See https://www.playframework.com/documentation/2.6.x/ScalaTestingWithScalaTest#Unit-Testing-EssentialAction for an example how this works. You'll need to use request.body.asJson.get inside your action to get the result.

By the way, I realise this is very confusing but unfortunately it's too hard to change Play to make it less confusing. There are several design issues coming together here:

  • Request extends RequestHeader - this causes issues all over the place, but we can't change it now
  • Action overloads the apply method for everything instead of having methods with separate names, e.g. accumulator(rh: RequestHeader) and invoke(r: Request[T])
  • AnyContent should only be used rarely but it's the default in our code and docs
  • Action is also an ActionBuilder鈥攁gain using apply methods鈥攁nd making it confusing to understand how Actions are built

I'm going to close this ticket now because it works as designed and we unfortunately can't change the design!

@richdougherty Thank you for your explanation, you save me a lot of time :)

This is one of my pet hates about Play, the overloading semantics are insane and poorly documented.

Was this page helpful?
0 / 5 - 0 ratings