Search code examples
jsonrestsymfonyfosrestbundle

FOSRestBundle - is a good practice to inject body params in the parameter bag?


We are using the FOS Rest bundle, and, at first, we were unaware that the body listener was active.

In the meanwhile, we created a lot of resources that are receiving data via the body, in JSON format. For now we only support JSON format. Lot of them now in the controllers are retrieving parameters from the Symfony ParameterBag, because, body listener was injecting them there.

So, do you think is a good practice to leave to body listener the responsibility of that, and in the controllers retrieve parameters via the parameter bag? This way, no matter via GET, POST or body, all the parameters arrive to the controllers via parameter bag.

We are looking at this because some of our API clients were making requests without providing the content-type in the header, and because of that, body listener didn't inject the body in the parameter bag. So in the controllers we didn't have the parameters available.

Thanks in advance!


Solution

  • Generally speaking, one of the most important aspects of API design should be consistency across your implementation, so that your consumers understand how to interact with your system. To that end, I would follow this pattern:

    • Keep the body listener enabled if it assists with your development; this listener is a shortcut to help you write simpler controllers, so if it is working for you, there's no reason to abandon it
    • For API endpoints that expect a JSON request body, report to the consumer that they are making an illegal request if the proper Content-type: application/json header is missing (which can be done via setting a _format requirement on the route; see advanced routing example)
    • If you must support these legacy users that are not passing the header, you could create a listener on the kernel.request event with a very high priority which simply checks for the detected mime type and, and if it is missing, does something like this:

      if ($request->getRequestFormat() != 'json') {
          json_decode($request->getContent());
          if (json_last_error() == JSON_ERROR_NONE) {
              $request->setRequestFormat('json');
          }
      }
      

      (The use of json_last_error() is not perfect here; see this question for a more detailed discussion.)

    • Finally, if you aren't already, make sure that you are restricting your API controllers to the proper request method, again to make sure that they are being consumed correctly and to make your development and use more consistent. If you are using annotations, for example, this is as simple as using the FOSRestBundle-provided @Get, @Post, etc. in place of the usual @Route. If you are using YML or XML configuration, it's the methods property.

      Although it is certainly possible for a single controller to accept data via a GET (with request parameters), a POST (via URL-encoded form data), or a POST or PUT with JSON data and a body listener-conversion, this makes for a very convoluted API, as you have to start maintaining these different cases, and I would argue that things become very difficult for your users as well.

    As a consumer of these APIs, I would expect my requests to fail if I were passing JSON in a request body but not including the proper header, and I would expect my requests to fail if I were passing x-www-form-urlencode data to a JSON-based API. Overall, both for the maintainer of an API and for the consumers, it is a good idea to be strict on what you accept and to follow the same pattern throughout.