⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
TL;DR: 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.
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
(end of note)
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:
- Run the jQuery AJAX POST/PUT/DELETE request
- It has to send an OPTIONS request with all CORS headers
- 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)
- 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
- 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
When I don't serialize the
data
withJSON.stringify
(see the Setup part above), the OPTIONS request is sent, but FOSRestBundle sends aBadRequestHttpException
sayingInvalid json message received
, and it's totally normal because the "request payload" (as seen in Chrome's developer tools) is the classicapplication/x-www-form-urlencoded
content, even if I specifiedcontentType: "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"
withapplication/x-www-form-urlencoded
ormultipart/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.