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.
(end of note)
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.
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).
Ideally, I want this:
application/json
response.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.
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.
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.
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?
2015-06-29 17:39
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.
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.