CORS error with 504 Gateway timeout

Overview

Like many developers, I often leave browser's "Developer Tools" open for websites of interests. Last week, while playing with our staging service, I saw repeated CORS errors in the Console tab and 504 Gateway Timeouts in the Network tab. It was my first time seeing these two errors together. So I decided to look into it a bit. In this blog, I will reproduce the issue and share some good practices handling CORS.

Explain CORS in one minute.

Cross-Origin Resource Sharing (CORS) is a safety mechanism that browser allows Javascript (JS) running in one origin to request resources from other origins. For example, the browser's address bar is https://app1.example.com and the JS on the page requests https://app2.example.com/user. In order for the request to succeed, the browser needs to know the server at app2.example.com is OK to serve requests made from app1.example.com. The server responds with access control HTTP-headers like Access-Control-Allow-Origin. If the headers do not match the browser, the browser rejects the server response.

When the request does not qualify as a simple request, the browser makes a preflight request to the server before the real request. Sometimes, the browser sends the request after the response of preflight arrives. Other times, the browser sends the request immediately after the preflight request, without blocking on the response of preflight request.

CORS error and Gateway Timeout

Now that we know what is CORS, let's look at the CORS error.

The Network tab shows 504 for the preflight request and "CORS error" for the real request.

The following is my hypothesis of what's going on:

  1. The server binds handler to URL path and uses the same code to handle the preflight request and the resource request.
  2. The server takes too long to respond the preflight request. This triggers the timeout configured at the API gateway. The API gateway sits between the browser and the server.
  3. The API gateway returns "504 Gateway Timeout" to the browser.
  4. The browser receives 504 with empty body. Because there is no Access-Control-Allow-Origin header, the browser reports CORS error.
  5. The browser also reports the 504 on the preflight request.

Reproduce

Let's examine the hypothesis by reproducing errors. I will use 3 components shown in the following diagram.

To setup the reproducing environment, download code and follow the README.

Step 1. Browser and the front end.

Host a webpage at http://localhost:8081 using Node JS. There is a button on the page and when we click it, the page makes the following fetch request to http://localhost:8082/hello.

 1fetch('http://localhost:8082/hello', {
 2    mode: 'cors',
 3    // Set a header so that the request does not qualify as simple request. 
 4    // That would force the FE to send preflight request. 
 5    headers: {
 6        "Header1": "Value1"
 7    },
 8})
 9    .then(response => response.text())
10    .then(data => console.log(data))
11    .catch(error => console.error('Error:', error));

Step 2. Nginx as the gateway.

Nginx runs at port 8082 and proxies requests /hello to http://localhost:8083/hello, with 2s timeout.

1server {
2        listen 8082;
3
4        location /hello {
5            proxy_pass http://localhost:8083/hello;
6            proxy_read_timeout 2s;
7        }
8}

Step 3. Origin server in Go.

Run a go web server that listens on port 8083. The handler for /hello sleeps 3 seconds before returning 200 Status OK.

1http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
2  // sleep long enough to trigger gateway timeout.
3    time.Sleep(3 * time.Second)
4  
5    log.Printf("%v \n%v\n", r.Method, r.Header)
6    w.Header().Add("Access-Control-Allow-Origin", "*")
7    w.Header().Add("Access-Control-Allow-Headers", "*")
8    fmt.Fprintf(w, "Hello, %v", time.Now())
9})

Step 4. Verify CORS with 504.

Clicks the button and we see the CORS errors and 504.

The CORS error from the console log.

The 504 Gateway Timeout on the preflight request and the CORS error on the real request.

If we increase the Nginx proxy timeout to 4 seconds, the request succeeds.

Good practices of handling CORS.

  1. If possible, make requests qualify simple request. This reduces the preflight requests.
  2. If possible, handle preflight requests at the gateway.
  3. If you have to handle preflight requests on the origin server, make sure the preflight request does not run the same code path as the real request. You can either
    check the HTTP Method is OPTIONS in the handler, or configure a different handler for requests with OPTIONS.