Cross-origin Resource Sharing

You are ready to start implementing a client for your bird-spotting service. Before reading further, you start up your service on port 5000.

Once the service is up and running, you sit down and write the following code to grab all the species from the get-all endpoint:

html
js
preview

This same request works fine when you test it with curl, but the fetch fails for some reason. Pop open this page in its own window and inspect the console. You find this error message:

Access to fetch at 'http://localhost:5000/species' from origin 'https://allover.twodee.org' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

This error message may not make any sense to you. The issue is that the browser is blocking https://allover.twodee.org from consuming a resource provided by a separate entity: your web service. The provider and consumer are two different origins. An origin is uniquely identified by these three fields:

If the provider and consumer differ in any of these fields, they are considered to be different origins.

By default, browsers hold a same-origin policy. They only allow a script to fully access responses to fetch if the URLs point to the same service that served out the script. Browsers block cross-origin requests to keep a web service's data from falling into the wrong hands.

The same-origin policy is too restrictive if a web service is meant for public consumption or consumption by a limited set of other origins. The browser will relax its policy if the response it gets from the service tells it to do so. Consider the headers of this blocked response to a GET request for /species:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 24
ETag: W/"18-IX2+HsABxBq+l51mJRXRA2fmxnA"
Date: Sun, 12 Sep 2021 11:52:58 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Compare it to this response, to which the browser will grant full access:

HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 24
ETag: W/"18-IX2+HsABxBq+l51mJRXRA2fmxnA"
Date: Sun, 12 Sep 2021 11:53:21 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Do you see the difference? The second response includes the header Access-Control-Allow-Origin. Its value is the wildcard *, which means that any consumer is allowed to access the response. If you want to open up your web service to the general public, you must add this header in your service. You do that by inserting your own middleware in your service script before defining any of your endpoints:

service.use((request, response, next) => {
response.set('Access-Control-Allow-Origin', '*');
next();
});

// define endpoints

Try adding this middleware to your Express service and restarting it. Inspect the headers of your GET request with this command:

curl --head http://localhost:5000/species

Do you see the Access-Control-Allow-Origin header? If so, reload this page. The error message in the Preview panel below is replaced by the JSON results of the query.

html
js
preview

If you only wanted to grant full access to the origin serving out this webpage, you would use https://allover.twodee.org instead of *.

This one extra header is enough to open up simple GET and POST requests to scripts. For other kinds of requests, browsers first issue a preflight request to see if the request is allowed. A preflight request is sent using the HTTP method OPTIONS. The web service's response must tell the client what methods and headers are allowed for cross-origin requests. Your service uses the GET, POST, PATCH, and DELETE methods, and clients might set the Content-Type header for requests with JSON bodies, so you add the following endpoint:

service.options('*', (request, response) => {
response.set('Access-Control-Allow-Headers', 'Content-Type');
response.set('Access-Control-Allow-Methods', 'GET,POST,PATCH,DELETE');
response.sendStatus(200);
});

Try adding this endpoint and restarting your service. Inspect the headers by running this command in your terminal:

curl --head --request OPTIONS \
http://localhost:5000/species

Since the URL for this endpoint is *, it will handle preflight requests for both /species and /species/:name.

Note that this endpoint doesn't actually send any data about birds back in the response body. It only reports back what kinds of requests the browser may send.

Many Express users do not manually insert the headers as you have done. They instead use the cors library.