How I Fixed: Missing Headers on Response in Symfony 3 API

The first time this caught me out, I didn’t feel so bad. The second time – i.e. just now – I knew I had already solved this problem (on a different project), and found my urge to kill rising.

I wanted to POST in some data, and if the resource is successfully created, then the response should contain a link – via a HTTP header – to the newly created resource.

Example PHP / Symfony 3 API controller action code snippet:

    public function postAction(Request $request)
    {
        $form = $this->createForm(MyResourceType::class, null, [
            'csrf_protection' => false,
        ]);

        $form->submit($request->request->all());

        if (!$form->isValid()) {
            return $form;
        }

        $myResource = $form->getData();

        $em = $this->getDoctrine()->getManager();
        $em->persist($myResource);
        $em->flush();

        $routeOptions = [
            'id'      => $myResource->getId(),
            '_format' => $request->get('_format'),
        ];

        return $this->routeRedirectView(
            'get_myresource', 
            $routeOptions, 
            Response::HTTP_CREATED
        );
    }

And from the front end, something like this:

export async function createMyResource(important, info, here) {

  const baseRequestConfig = getBaseRequestConfig();

  const requestConfig = Object.assign({}, baseRequestConfig, {
    method: 'POST',
    body: JSON.stringify({
      important,
      info,
      here
    })
  });

  /* global API_BASE_URL */
  const url = API_BASE_URL + '/my-resource';

  const response = await asyncFetch(url, requestConfig);

  return {
    myResource: {
      id: response.headers.get('Location').replace(`${url}/`, '')
    }
  };
}

Now, the interesting line here – from my point of view, at least – is the final line.

Because this is a newly created resource, I won’t know the ID unless the API tells me. In the Symfony controller action code, the routeRedirectView  will take care of this for me, adding on a Location header pointing to the new resource / record.

I want to grab the Location  from the Headers returned on the Response and by removing the part of the string that contains the URL, I can end up with the new resource ID. It’s brittle, but it works.

Only, sometimes it doesn’t work.

Response
body:(...)
bodyUsed: false
headers: Headers
  __proto__: Headers
ok:true
status:201
statusText:"Created"
type:"cors"
url:"http://api.my-api.dev/app_dev.php/my-resource"
__proto__:Response

Excuse the formatting.

From JavaScript’s point of view, the Headers array is empty.

This leads to an enjoyable error: “Cannot read property ‘replace’ of null”.

Confusingly, however, from the Symfony profiler output from the very same request / response, I can see the header info is there:

Good times.

Ok, so the solution to this is really simple – when you know the answer.

Just expose the Location  header 🙂

# /app/config/config.yml

# Nelmio CORS
nelmio_cors:
    defaults:
        allow_origin:  ["%cors_allow_origin%"]
        allow_methods: ["POST", "PUT", "GET", "DELETE", "OPTIONS"]
        allow_headers: ["content-type", "authorization"]
        expose_headers: ["Location"] # this being the important line
        max_age:       3600
    paths:
        '^/': ~

After that, it all works as expected.