Access to the raw Request body in Play Framework

Access to the raw Request body in Play Framework

Play Framework uses the concept of BodyParsers to read the data being sent in our HTTP requests. You can read more about this subject in the official docs.

As it can be seen, we can specify an explicit BodyParser in our controller actions as explained in this page but unless we have some specific need we will possibly be better delegating the right parser to the framework.

By default, Play uses the BodyParser.AnyContent class which tries to guess the best way to parse our data. Essentially, it looks at the Content-Type header in order to find out the most suitable parser and once the data stream is parsed we will have our data accessible via convenient methods.

But here is when things start to turn interesting and tricky. Play Framework uses streams to represent the HTTP request and response and this means that once the stream is parsed, you cannot parse it again. If you think about it, it makes sense but IMHO the API can be a bit misleading.

Let’s imagine we send a POST request with a Content-Type header application/json and some JSON body. As stated in the docs, if we want to access the body, we can use:

1JsonNode body = request().body().asJson();

And we will see the expected JSON body there. Play Framework uses the Jackson library by default, so we can access all the properties in a very easy way.

However, JSON is loosely typed and it has some quirks when trying to parse arbitrary data. We actually had a bit of a WTF situation last week and this was the motivation to write this post.

Imagine we are sending a JSON body which includes a double value like this:

2   "someDoubleValue": 1.0

The raw JSON body travelling through the network will contain the 1.0 as expected but if we look at what Jackson has parsed, we will see that it has converted 1.0 to 1.

1String jsonToString = request().body().asJson().toString(); 
2// This String will be {"someDoubleValue": 1}

Arguably, this should not matter at all, and in reality, if we use one of the Jackson convenient methods, it will parse the data accordingly and we won’t even notice in 99% of the cases

1double someDoubleValue = request().body().asJson().get("someDoubleValue").asDouble();
2// We will have our 1.0 again

In our case, we are sending a special HTTP header which is a hash of the Json body. This hash is validated in the server, so the raw body should match.

The problem is that BodyParser.AnyContent will only provide the data in the format it believes is right, and a very misleading null in any other convenience methods

1JsonNode body = request().body().asJson(); // Our JSON data
2String body = request().body().asText(); // NULL unless the Content-Type is text/plain
3RawBuffer buff = request().body().asRaw(); // NULL if the Content-Type satisfies some of the other convenient access methods

We could argue that this may be a not great API, and we should have access to the raw body no matter what but Play Framework has worked like this for a while and it does not seem to be changing any soon.

So, the only way to get back the original array of bytes in our HTTP Request body in order to be able to correctly calculate the hash is to force the use of the BodyParser.Raw class

And the way to do that is to add an annotation to our controller definition

 1public class MyController {
 2   private static final ObjectMapper objectMapper = new ObjectMapper();
 4   @BodyParser.Of(BodyParser.Raw.class)
 5   public Result MyAction() {
 6        JsonNode requestData;
 7        String rawBody = "";
 8        try {
 9            rawBody = new String(request().body().asRaw().asBytes(), "UTF-8");
10            requestData = objectMapper.readValue(rawBody, new TypeReference<JsonNode>() {
11            });
12        } catch (Exception e) {
13            throw new RuntimeException("Body was not a json: " + rawBody);
14        }
16        // And at this point requestData will be the same as request().body().asJson()
17        // and rawBody will be the exact stringified JSON that was sent to the server
18   }

All this information can be obtained reading carefully the docs but I hope a more specific post like this may be able to save some headaches to someone like it happened to us!