cerhovice
cerhovice

Reputation: 686

jQuery CORS request dies on preflight OPTIONS request in FireFox

I can't tell where the breakdown occurs, whether this is a jQuery problem or a Compojure problem or what.

I would like to make this cross-domain request:

function signup() {

var signup_username = $('#signup_username').val(); 
var signup_password_1 = $('#signup_password_1').val(); 
var signup_password_2 = $('#signup_password_2').val(); 

$.ajax({
    type: "POST",
    contentType: "application/json",
    url: "http://localhost:40000/signup",
data: 
    JSON.stringify({
        "signup_username": signup_username,
        "signup_password_1": signup_password_1,
        "signup_password_2": signup_password_2
    }),

    complete: function (data) { console.log(data); alert("Done. Look at the console.log to see the results.");  },
    success: function (data) { console.log(data);  },
    error:  function (data) { console.log(data); },
    dataType: "json"
});
}

I write a small Clojure app on the server, using embedded Jetty as the server. I define my Compojure routes like this:

(defroutes app-routes
  (GET "/" request (login-form request))
  (POST "/" request (login request))
  (OPTIONS "/" request (preflight request))
  (GET "/signup" request (signup-form request))
  (POST "/signup" request (signup request))
  (OPTIONS "/signup" request (preflight request))
  (route/resources "/")
  (route/not-found "Page not found. Check the http verb that you used (GET, POST, PUT, DELETE) and make sure you put a collection name in the URL, and possibly also a document ID."))

I am testing on my local machine, but I want to test cross-domain, so I spin up the same app on 2 different ports: 34001 and 40000. I will point FireFox at 34001 and then the Javascript should make a cross-domain call to 40000.

First, I test this with CURL:

curl -X OPTIONS --verbose  http://localhost:40000/signup

gives me:

* About to connect() to localhost port 40000 (#0)
*   Trying ::1... connected
* Connected to localhost (::1) port 40000 (#0)
> OPTIONS /signup HTTP/1.1
> User-Agent: curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8x zlib/1.2.5
> Host: localhost:40000
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Sun, 13 Jul 2014 20:43:43 GMT
< Access-Control-Allow-Origin: localhost:40000
< Access-Control-Allow-Methods: PUT, DELETE, POST, GET, OPTIONS, XMODIFY
< Access-Control-Max-Age: 2520
< Access-Control-Allow-Credentials: true
< Access-Control-Request-Headers: x-requested-with, content-type, origin, accept
< Access-Control-Allow-Headers: x-requested-with, content-type, origin, accept
< Content-Type: application/json;charset=ISO-8859-1
< Content-Length: 12
< Server: Jetty(7.x.y-SNAPSHOT)
< 
* Connection #0 to host localhost left intact
* Closing connection #0

So I see most of the headers that I expect to see.

Now I test it with FireFox, with Firebug enabled. Firebug does not show any preflight OPTIONS request being made, instead I simply see the error:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:40000/signup. This can be fixed by moving the resource to the same domain or enabling CORS.

Strange. I have to wonder if FireFox is making the preflight OPTIONS request. So I start up Charles http://www.charlesproxy.com/

Charles shows me activity that FireBug does not. Charles shows me this request:

OPTIONS /signup HTTP/1.1
Host: localhost:40000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:30.0) Gecko/20100101 Firefox/30.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://localhost:34001
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

and Charles shows me this response:

HTTP/1.1 200 OK
Date: Sun, 13 Jul 2014 20:35:27 GMT
Access-Control-Allow-Origin: http://localhost:34001
Access-Control-Allow-Methods: DELETE, GET, POST, PUT
Content-Type: application/octet-stream;charset=ISO-8859-1
Content-Length: 18
Server: Jetty(7.x.y-SNAPSHOT)

preflight complete

At least one of the problems I face is, where did this response come from? As you can see from my Compojure routes above, this request should have gone to the "preflight" function, which I defined like this:

(defn preflight [request]
  "2014-07-13 - this is meant to enable CORS so our frontenders can do cross-browser requests. The browser should do a 'preflight' OPTIONS request to get permission to do other requests."
  (print " IN PREFLIGHT ")
  (println " headers host is: " (str (get-in request [:headers "host"])))
  (assoc
      (ring.util.response/response "CORS enabled")
    :headers {"Content-Type" "application/json"
              "Access-Control-Allow-Origin" (str (get-in request [:headers "host"]))
              "Access-Control-Allow-Methods" "PUT, DELETE, POST, GET, OPTIONS, XMODIFY" 
              "Access-Control-Max-Age" "2520"
              "Access-Control-Allow-Credentials" "true"
              "Access-Control-Request-Headers" "x-requested-with, content-type, origin, accept"
              "Access-Control-Allow-Headers" "x-requested-with, content-type, origin, accept"}))

I can see these headers when I tested this with CURL, but not when I use FireFox. And also my printlin statements are printed to the terminal when I use CURL, but not FireFox.

For some reason, the preflight request from FireFox does not go to my preflight function. I can not figure out where it goes, actually. The response it gets does not match any route that I've defined.

Anyone have any ideas what is going on?

I notice that this fellow tried his best and decided that CORS is unusable:

http://chstrongjavablog.blogspot.com/2013/04/enabling-cors-for-jetty.html

UPDATE:

this looks suspicious:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

How do I say "application/json"?

How do I get control over the preflight request that FireFox makes? Or is it jQuery that does this?

Also, this response looks very limited:

Content-Type: application/octet-stream;charset=ISO-8859-1

Again, I can't figure out what is causing this return, so I don't have much control over the response.

When I test this in Chrome, I get this error:

 XMLHttpRequest cannot load http://localhost:40000/signup. Request header field Content-Type is not allowed by Access-Control-Allow-Headers. 

And, again, when I test this using CURL, I get the correct headers, and yet somehow Chrome and FireFox are going to a route that I have never defined.

If I do this:

curl -X OPTIONS --verbose  http://localhost:40000/signup

I get these responses headers:

< HTTP/1.1 200 OK
< Date: Sun, 13 Jul 2014 22:45:00 GMT
< Access-Control-Allow-Origin: localhost:40000
< Access-Control-Allow-Methods: PUT, DELETE, POST, GET, OPTIONS, XMODIFY
< Access-Control-Max-Age: 2520
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: x-requested-with, content-type, origin, accept
< Content-Type: application/json;charset=ISO-8859-1
< Content-Length: 12
< Server: Jetty(7.x.y-SNAPSHOT)

That includes Access-Control-Allow-Headers which has "content-type". But in both Chrome and FireFox, the browsers do a preflight OPTIONS call that gets a response that does not seem to come from any route that I have defined.

If I use the Charles network debugger, I see FireFox send these headers:

OPTIONS /signup HTTP/1.1
Host: localhost:40000
Access-Control-Request-Method: POST
Origin: http://localhost:34001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
Access-Control-Request-Headers: accept, content-type
Accept: */*
Referer: http://localhost:34001/signup
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8

and get this response:

HTTP/1.1 200 OK
Date: Sun, 13 Jul 2014 22:42:58 GMT
Access-Control-Allow-Origin: http://localhost:34001
Access-Control-Allow-Methods: DELETE, GET, POST, PUT
Content-Type: application/octet-stream;charset=ISO-8859-1
Content-Length: 18
Server: Jetty(7.x.y-SNAPSHOT)

preflight complete

I have no idea where that response comes from. If I do this on my whole project:

grep -iR "preflight complete" *

The phrase "preflight complete" appears no where is my code. So where is this response coming from? And why don't FireFox and Chrome end up at the same route as when I make my call in CURL?

UPDATE: I finally found the answer.

I was thrashing about for several hours, trying to get CORS to work, and among many other experimentations, I applied this middleware:

https://github.com/r0man/ring-cors

and that overrode the headers I was sending back. I am curious why this override only happened for Ajax requests and not curl requests. Maybe that is some magic r0man put into ring-cors. Either way, I deleted it and now I see the headers I am sending back.

Upvotes: 4

Views: 3188

Answers (1)

Goose3gg
Goose3gg

Reputation: 93

You are going to have to handle two requests, the first being an options and the second being a post. Luckily, someone has written a framework to make this easy (https://github.com/r0man/ring-cors).

If you decide to use ring-cors, then you can wrap all your routes by:

(def app 
  (-> (handler/api app-routes)
      (wrap-cors :access-control-allow-origin #"yoursite"
                 :access-control-allow-methods [:get :put :post]
                 :access-control-allow-headers ["Content-Type"])))

Upvotes: 4

Related Questions