Alex Pierstoval Alex Pierstoval - 6 months ago 140
Javascript Question

CORS with Symfony, jQuery, FOSRestBundle and NelmioCorsBundle

Edit (before reading the whole thing):




This issue is solved because I made a very stupid mistake in my code, not related to CORS or anything.

If you want to read this issue anyway, just note that it has a working CORS configuration, if you might want to take a good example.


Situation



I have a multi-domain Symfony application, and the back-end part updates data with web-services, not classic forms, for some reasons.

Back-end: back.mydomain.dev
Webservices domain: api.mydomain.dev

I am using jQuery to make AJAX calls to these webservices, and if I want to modify or create objects, I also send AJAX requests, the objects are merged with Doctrine entities and persisted.

I have been fighting for an entire year to make GET, PUT, POST and DELETE requests that would work properly on this application, and just because they're on different domains, I am forced to setup CORS on my different environments.

Setup



All jQuery AJAX request look like this:

ajaxObject = {
url: 'http://api.mydomain.dev/' + uri,
type: method, // Can be GET, PUT, POST or DELETE only
dataType: 'json',
xhrFields: {
withCredentials: true
},
crossDomain: true,
contentType: "application/json",
jsonp: false,
data: method === 'GET' ? data : JSON.stringify(data) // Here, "data" is ALWAYS containing a plain object. If empty, it equals to "{}"
};

// ... Add callbacks depending on requests

$.ajax(ajaxObject);


Behind, the routes are managed with Symfony.

For CORS configuration, I am using NelmioCorsBundle with this configuration:

nelmio_cors:
paths:
"^/":
allow_credentials: true
origin_regex: true
allow_origin:
- "^(https?://)?(back|api)\.mydomain.dev/?"
allow_headers: ['Origin','Accept','Content-Type']
allow_methods: ['POST','GET','DELETE','PUT','OPTIONS']
max_age: 3600
hosts:
- "^(https?://)?(back|api)\.mydomain.dev/?"


The controller used is extending FOSRestBundle's one, has some security (for example, cannot POST/PUT/DELETE when you don't have the correct role), can update objects and returns only JSON data (there is a listener for that).

Ideal behavior



Ideally, I want this:


  1. Run the jQuery AJAX POST/PUT/DELETE request

  2. It has to send an OPTIONS request with all CORS headers

  3. NelmioCorsBundle should return the correct CORS headers accepting the request to be done, even before running any controller inside the app (made by the bundle's request listener)

  4. If accepted, the proper HTTP request is sent to the controller with all request data as a serialized JSON string (in the request payload), and Symfony retrieves it and interprets the correct JSON string as an array object

  5. The controller then gets the data, does its stuff, and returns an
    application/json
    response.



Problem



But here, I tried maaaaany combinations, and I can't seem to make it work.

The point n°3 and 4 are failing, completely or partially.

Tried workarounds




  1. When I don't serialize the
    data
    with
    JSON.stringify
    (see the Setup part above), the OPTIONS request is sent, but FOSRestBundle sends a
    BadRequestHttpException
    saying
    Invalid json message received
    , and it's totally normal because the "request payload" (as seen in Chrome's developer tools) is the classic
    application/x-www-form-urlencoded
    content, even if I specified
    contentType: "application/json"
    in the jQuery AJAX request, whereas it should be a serialized JSON string.

  2. However, if I do serialize the
    data
    var, the "request payload" is valid, but the OPTIONS request is not send, making the whole request fail because of the lack of CORS acceptance.

  3. If I replace
    contentType: "application/json"
    with
    application/x-www-form-urlencoded
    or
    multipart/form-data
    , and don't serialize the data, then, the request payload is valid, but the OPTIONS request is not sent. It should also be normal, as explained in jQuery's docs under the contentType parameter:


    Note: For cross-domain requests, setting the content type to anything other than application/x-www-form-urlencoded, multipart/form-data, or text/plain will trigger the browser to send a preflight OPTIONS request to the server.


    But then, how to send a correct CORS request?



Questions




  • Where does this issue comes from?

  • Is this a problem with my setup? With jQuery?

  • With AJAX itself?

  • What can be the different solutions to solve this damn issue?



Edits (after comments)



2015-06-29 17:39

Conditions:



In AJAX, the
contentType
is set to
application/json
.
The
data
option is set to a serialized JSON string.

Preflight REQUEST headers

OPTIONS http://api.mydomain.dev/fr/object/1 HTTP/1.1
Access-Control-Request-Method: POST
Origin: http://back.mydomain.dev
Access-Control-Request-Headers: accept, content-type
Referer: http://back.mydomain.dev/...


Preflight RESPONSE headers

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST, GET, DELETE, PUT, OPTIONS
Access-Control-Allow-Headers: origin, accept, content-type
Access-Control-Max-Age: 3600
Access-Control-Allow-Origin: http://back.mydomain.dev





POST
REQUEST headers (after preflight)

POST http://api.mydomain.dev/fr/object/1 HTTP/1.1
Origin: http://back.mydomain.dev
Content-Type: application/json
Referer: http://back.mydomain.dev/...
Cookie: mydomainPortal=v5gjedn8lsagt0uucrhshn7ck1
Accept: application/json, text/javascript, */*; q=0.01


Request payload (raw):
{"json":{"id":1,"name":"object"}}


Must be noted that this throws a javascript error:


XMLHttpRequest cannot load http://api.mydomain.dev/fr/object/1. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://back.mydomain.dev' is therefore not allowed access.


POST
RESPONSE headers (after preflight)

HTTP/1.1 200 OK
Date: Mon, 29 Jun 2015 15:35:07 GMT
Server: Apache/2.4.12 (Unix) OpenSSL/1.0.2c Phusion_Passenger/5.0.11
Keep-Alive: timeout=5, max=91
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8





2015-06-29 17:39

I also tried using vanilla javascript to make the XMLHttpRequest, the problem is still the same:



var xhr = new XMLHttpRequest;
xhr.open('POST', 'http://api.mydomain.dev/fr/objects/1', true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.withCredentials = true; // Sends cookie
xhr.onreadystatechange = function(e) {
// A simple callback
console.info(JSON.parse(e.target.response));
};
// Now send the request with the serialized payload:
xhr.send('{"id":1,"name":"Updated test object"}');


Then, the browser sends an OPTIONS request:

OPTIONS http://api.mydomain.dev/fr/objects/1 HTTP/1.1
Connection: keep-alive
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Origin: http://back.mydomain.dev


Which returns the correct Response headers:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST, GET, DELETE, PUT, OPTIONS
Access-Control-Allow-Headers: origin, accept, content-type
Access-Control-Max-Age: 3600
Access-Control-Allow-Origin: http://back.mydomain.dev


But then, the browser sends the POST request without any "Access-Control-*" header.

Answer

Actually, my setup worked well.
Absolutely perfectly, if I might say it.

It was just some kind of... Stupidity.

An exit; was hidden deep in a PHP file.

Sorry for annoyance. I guess it's kind of a record in a text/quality ratio on SO.

Edit: I still hope that this issue can be a proper example of a fully CORS-compatible Symfony project.

Comments