Cross-origin Resource Sharing — A Hands-on Tutorial (Part II : Complex Requests)
Continuing from Part-I where we successfully handled a cross-origin “simple” request, lets see what non-simple requests are and what we can do to enable support for such requests.
Example #2 — Complex Requests
Starting from where we left off. We were running a PageServer serving HTML pages and an APIServer serving GetUser
API call to return User JSON.
Lets see what happens when we now try to create a user using Javascript
- Point your browser to
createUser.html
. You should see something like this
So, let’s add some string data in the form fields and click “submit” button. This should convert the data to JSON and do a POST to http://apiserver.cors.com:12346/users with the json data as the body of the request
fetch('http://apiserver.cors.com:12346/users', {
method: 'post',
body: JSON.stringify(user),
headers: {
"Content-type": "application/json"
},}).....
However, what you get is this error
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://apiserver.cors.com:12346/users. (Reason: header ‘content-type’ is not allowed according to header ‘Access-Control-Allow-Headers’ from CORS preflight response).
What’s going on?
Pre-Flight
What we called as “complex” request actually causes two
HTTP requests under the covers.
- The browser first issues a preflight or an OPTIONS request, which is basically asking the server for permission to make the actual request.
- Once permissions have been granted, the browser makes the actual request.
So, in this case, the pre-flight request is something like below
OPTIONS /users HTTP/1.1
Host: apiserver.cors.com:12346
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Referer: http://pageserver.cors.com:12345/createUser.html
Origin: http://pageserver.cors.com:12345
Connection: keep-alive
The preflight request contains a few additional headers:
Access-Control-Request-Method
— The HTTP method of the actual request.Access-Control-Request-Headers
— A comma-delimited list of non-simple headers that are included in the request. Notice that all CORS related headers are prefixed with “Access-Control-”.
In order for the POST to succeed, the server should support this request, “granting” permission based on the above request headers.
TIP: Unfortunately Chrome developer tool no longer shows the pre-flight call , see here https://stackoverflow.com/questions/57410051/chrome-not-showing-options-requests-in-network-tab . If you would like to see the OPTIONS request details, just use Firefox Debugger (or use chrome://net-internals)
Solution
In order for the POST to succeed, the server should support this request, “granting” permission based on the above request headers.
- Stop apiserver/allow_origin/apiserver.go and Start
preflight
server
$ go run apiserver/preflight/apiserver.go
What we have done here is added some code in the apiserver to respond to OPTIONS request, granting the permission for GET, and POST calls with Content-Type header.
........
if (r.Method == "OPTIONS") && (reqMethod == "GET" || reqMethod == "POST") && (strings.EqualFold(reqHeader, "Content-Type"))
{
w.Header().Set("Access-Control-Allow-Methods", "POST, GET")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
return
}
.....
Full code here
- Enter data and hit “create” button again. You will see that the request succeeded.
Debugger/Dev Tools will show that the response headers from Server granted the needed permission as response to OPTIONS request
HTTP/1.1 200 OK
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Origin: http://pageserver.cors.com:12345
Date: Mon, 01 Jun 2020 02:16:13 GMT
Content-Length: 0
In addition, the server can also return a header called Access-Control-Max-Age
. The value of the header indicates how long the pre-flight response can be cached by the browser and hence browsers can skip the check for that duration
Hope this makes this a little bit easier when you are dealing with CORS failures . They can be really opaque (esp. now with Chrome dev tools not even showing pre-flight calls)
In the next part we will look at handling credentials in a CORS request