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.