Making cross-origin requests with Fetch

Anton Ioffe - October 13th 2023 - 17 minutes read

As professional web developers striving to navigate the labyrinth of JavaScript and modern web protocols, few challenges are quite as pervasive as managing Cross-Origin Requests. In the interconnected ecosystem of today's web, where data sharing and service integration have become the lifeblood of rich, interactive web applications, understanding and properly implementing these requests is now a crucial skill-set.

We're excited to bring you this in-depth guide exploring how to make cross-origin requests with one of the most powerful APIs in modern web development, Fetch. Diving beneath the surface, we'll dissect every detail, from the meaning of cross-origin requests and their restrictions, to the granular mechanisms that govern them within Fetch.

Expect the mysteries behind CORS (Cross-Origin Resource Sharing), preflight requests, and the role of HTTP headers to unfold. Our discourse further delves into common errors, troubleshooting strategies, the role of the server in CORS and finally, the critical security considerations. If your work comprises of crafting intricate web experiences with JavaScript, this is an exploration you cannot afford to miss. It not only adds a powerful tool to your coding arsenal but equips you to navigate the web's intricate data sharing framework securely and confidently.

Unveiling Cross-Origin Requests

First, let's understand the core concept of 'origin', a crucial concept connected to cross-origin requests. In computing, 'origin' is defined as a combination of the protocol (http, https), the host or domain name (example.com), and the port number (80, 443). Thus origin could be characterized as a domain/port/protocol triplet.

For instance, http://example.com, https://example.com, http://example.com:8080, and https://example.com:3000 would be considered different origins even though they point to the same server. To put it simply, two URLs sharing the same origin means that both have the same protocol, the same domain, and the same port number.

Now that we've given a clear definition of 'origin', let's move onto the 'same-origin policy.' This is a critical security feature implemented in web browsers that restricts web pages from making requests to a different origin than the one that originally served the page. The same-origin policy helps mitigate certain types of security risks, such as malicious scripts stealing sensitive information.

One might wonder, "Why do we have to go through all this? Why can't we just fetch data directly from any domain we want?". The answer is simple: while such a restriction might seem inconvenient for developers, it's crucial for web security. Without such a policy, a malicious script from one site could potentially access and manipulate data on another site. For example, an evil script from a hacker website could access a user's mailbox on Gmail, leading to a serious security breach.

This is where the concept of cross-origin requests comes into play. Cross-origin requests are those sent to a different domain (even a different subdomain), protocol, or port than the one from which the initial request was made. These types of requests require particular headers from the remote side, a measure enforced by the CORS (Cross-Origin Resource Sharing) policy.

Here's a simple example illustrating how cross-origin requests work:

// the user visits http://example.com
// the page tries to fetch some data from http://service.example.com using JavaScript
let response = fetch('http://service.example.com/data');

// The browser sends a GET request to http://service.example.com 
// with an Origin header indicating the origin of the current page
// Origin: http://example.com

// The server at http://service.example.com responds with
// the data and an Access-Control-Allow-Origin header
// that specifies which origins are allowed to access the resource
// Access-Control-Allow-Origin: http://example.com 

In this workflow, keep your attention on the Access-Control-Allow-Origin response header from the server - this Header specifies which origins are allowed to access the resource.

Through this detailed outline of the origin, same-origin policy, and cross-origin requests, we can appreciate the delicate balance between enhancing web functionality and preserving security. Cross-origin requests have been a stepping-stone in enhancing the web's potencies while making sure that data integrity and confidentiality remain uncompromised.

Always remember, each domain from which we request data interprets our request's origin with regard to its own security policies before choosing to cooperate or not. Thus, while cross-origin requests provide avenues for inter-domain communication, their operation still greatly depends on the decisions made at the server end.

So now, what answers can we provide for the cases when our legitimate cross-origin requests fail? How can we design our application's code to comply with these security measures more effectively? These would be excellent food for thought as we further delve into understanding the structure of web APIs.

With these concepts in mind, we're well-prepared to move forward, unveiling more complexities, best practices, and gotchas of working with cross-origin requests in JavaScript. And as we do so, we'll continue to see how these processes underline the intricate balance between preserving the webs' openness and ensuring its secure operation.

Understanding CORS in Depth

Cross-Origin Resource Sharing (CORS) is a protocol that enables resources on a web page, such as fonts or JavaScript, to be requested from a different domain than the domain from which the original resource originated. Essentially, CORS utilizes additional HTTP headers to inform browsers that a web application operating at one origin has permission to access selected resources from a different origin.

This topic, however, cannot be completely understood without first digging into the issue that inspired the creation of CORS: The same-origin policy.

Same-Origin Policy, and the Need for CORS

The same-origin policy is a crucial web security mechanism that restricts how a document or script loaded from one origin can interact with a resource from another origin. This policy limits a web page to accessing resources from the same domain, protocol, and port, also referred to as the same origin.

While this precaution aids in safeguarding against potential security threats, it does pose challenges when it's necessary to make legitimate cross-origin requests. To elucidate, if a script located at http://websiteA.com endeavors to send an AJAX request to http://websiteB.com/data.json, the same-origin policy would block it. They have different origins due to the distinct domains they belong to – websiteA.com and websiteB.com.

A Modern Solution: CORS

In an attempt to circumvent these restrictions while maintaining secure browser-server interactions, CORS was introduced. In essence, it enables a server to relax the strict rules of the same-origin policy by stating, "Even though you are from a different origin, I will still accept your request".

The CORS protocol essentially consists of two significant steps: The client request and the server response.

1. Client (Browser) Request

When a browser sends a cross-origin request, it includes an Origin header that contains the protocol, host, and port number. The browser is responsible for setting this header.

GET /data HTTP/1.1
Host: websiteB.com
Origin: http://websiteA.com
...

2. Server Response

If the server at websiteB.com is configured to accept cross-origin requests from websiteA.com, it will include an Access-Control-Allow-Origin header in its response.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://websiteA.com
...

In the response above, if the server echoes back websiteA.com in the Access-Control-Allow-Origin header, the browser will allow the response data to be shared with the client-side script on websiteA.com. Alternatively, the server can respond with a *, effectively permitting any site to access the resource.

Going Deeper: Understanding CORS Headers

While the general CORS flow consists of a client request and a server response, an in-depth understanding also requires examining other critical CORS headers. For example, Access-Control-Allow-Credentials indicates whether the browser should expose response to frontend JavaScript code when credentials are included, while Access-Control-Allow-Methods defines the methods that are allowed when accessing the resource. Additionally, Access-Control-Allow-Headers specifies which HTTP headers can be used during the actual request.

With an improved understanding of CORS and its workings, web developers can leverage this powerful protocol to interact more robustly and securely with external resources – a fundamental requirement in the age of complex web applications.

Fetch with CORS and Credentials

Fetch with CORS and Credentials

When employing the Fetch API to formulate cross-origin requests that include credentials, certain settings must be customized in the fetch() function's second parameter. These primarily involve the mode and credentials options.

The role of the mode option is to manage the CORS tactics for the request. Configuring mode: 'cors' directs fetch to prompt the browser to examine the CORS policy in the server's response headers and abide by it.

fetch('https://example.com/api', {
 method: 'POST', 
 mode: 'cors', // Dictate to include CORS headers
 ...
})

Though this is the default operation, other mode settings are also at your disposal. Establishing mode: 'no-cors' instructs fetch to classify the server's response as opaque, essentially blocking any assessment of its content or status. Conversely, the mode: 'same-origin' setting limits fetch requests to the current page's origin, blocking any cross-origin requests.

Let's take a step further and assume a scenario that necessitates including credentials, such as cookies, with the request. For this, we leverage the credentials option and set it as 'include'.

fetch('https://example.com/api', {
 method: 'POST',
 mode: 'cors',
 credentials: 'include', // Mandate to include credentials with the request
 ...
})

Once credentials are attached to a request, both the client-side JavaScript and server-side configurations must align to accommodate these credentials. On the server-side, incoming credentials are accepted by responding with headers that possess Access-Control-Allow-Credentials: true.

// Server response headers
"Access-Control-Allow-Origin": "https://yourdomain.com",
"Access-Control-Allow-Credentials": "true"

Make sure not to overlook that Access-Control-Allow-Origin must match your domain exactly when set alongside credentials. A wildcard (*) cannot be employed here.

Given the restriction - wildcards are disallowed for Access-Control-Allow-Origin while dealing with credentials, what if you have multiple services hosted on numerous domains? How would you modify your CORS policy in response to such a requirement?

Being well-versed with CORS handling mechanisms within the Fetch API is pivotal for secure and successful interoperation with various services via their APIs. Test the waters and scrutinize these routines using the Fetch API at your own pace, tweaking parameters and monitoring outcomes to gain an intimate understanding of their behaviors.

Demystifying Preflight Requests

Preflight requests, also known as CORS preflight requests, are a unique aspect of how requests are handled on the web when it comes to modern JavaScript development. These requests play a critical role in ensuring the safety and integrity of web communications, particularly when dealing with cross-origin requests.

Cross-origin resource sharing (CORS) is an essential part of web security, controlling how resources are shared across different origins. A common scenario where CORS come into play is when your JavaScript code, hosted on one server, attempts to request resources from an API hosted on a different server, essentially a cross-origin request.

Normally, the browser's same-origin policy restricts these kinds of requests for the sake of security, preventing potentially malicious requests from reaching our APIs and databases. However, CORS provides a set of mechanisms that allows controlled relaxation of this policy under certain circumstances.

What Makes Preflight Requests Special?

The unique aspect of preflight requests is that they're sent before the actual request. It's as if your JavaScript sends a preliminary request to the server saying, "Hey, I'm about to send a real request your way. Is it okay if I go ahead with it?"

The preflight request is conveyed via the OPTIONS HTTP method. Instead of a GET or POST request, which carries data or instructions, an OPTIONS request asks the server for details about what other HTTP methods and headers it allows. It's not the normal data-fetching request we are used to but a request about requests. A little meta, but incredibly useful.

This preflight request includes certain headers, most notably Access-Control-Request-Method and Access-Control-Request-Headers. These headers let the server know what kind of request is about to be sent (the method) and what kinds of headers the upcoming request will contain.

//Example of a preflight request
OPTIONS /data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: DELETE 

How Does the Server Respond?

The server responds to the preflight request with information about what it accepts from this particular origin via response headers. The Access-Control-Allow-Origin response header indicates which origin sites can share its resources. It must either contain the origin of the requesting site or a * to indicate that any site may access the resource.

//Sample server response to a preflight request
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, DELETE, HEAD, OPTIONS 

This response also includes the Access-Control-Allow-Methods header, which lists all HTTP methods that the client is permitted to use, along with Access-Control-Allow-Headers, which indicates which HTTP headers can be used when making the actual request.

Does Preflight Happen All the Time?

The answer is, not always. Preflight only happens for non-simple HTTP requests. These are requests that might have an impact on user data. In other words, preflights are not needed for GET requests but are necessary for POST, PUT or DELETE requests. These types of requests potentially modify user data on the server, so the browser is checking to see if the server will permit them by sending the preflight OPTIONS request first.

Finally, the server response can include an Access-Control-Max-Age header, which specifies how long the preflight response can be cached. This means the browser can remember the server's response for a certain period of time and won't need to send a preflight request for every single request.

This "double checking mechanism" is what makes preflight requests valuable in our web development toolkit – it allows us to identify what kind of requests are permissible, and protects our application from potential security threats.

A single question that you, as a developer, might ask is: ‘How can I optimize the preflight process to ensure efficient communication between browsers and servers?’ Taking a deep dive into preflight requests is one way to start answering this question.

CORS Errors and How to Handle Them

As you delve into executing cross-origin HTTP requests using Fetch API, two common CORS errors might frequently disrupt the workflow — Third-party Cookie Error and Preflight Request Failure. Let's explore these problems and their solutions in-depth.

Third-party Cookie Error

In cross-origin requests, the management of third-party cookies often leads to complications. When unwrapped, an ordinary Fetch request should appear like this:

fetch('https://my-api.com', {
  method: 'GET',
  credentials: 'include'
})
.then(response => response.json())
.catch(error => console.error('CORS error:', error));

Here, the inclusion credentials: 'include' in the Fetch function is implemented to control cookies. Complications often arise due to restrictions laid by server configurations and browser settings that deter the proper management of third-party cookies.

Solution: To address this issue, you can take the following steps:

  1. Server-side: Mandate your server to establish the 'Access-Control-Allow-Credentials' response header to 'true'. This tells the browser it's permitted to include cookies, authorization headers, or TLS client certificates during cross-origin requests.
  2. Client-side: Verify to allow third-party cookies in your browser settings. However, you should note that readily allowing third-party cookies could expose your application to potential security hazards.

Remember, this approach's success would largely depend on both your server configuration and browser settings. In some cases where you have no control over the API server, fixing the cookie error could be challenging.

Preflight Request Failure

In cases where the CORS request might bring about data modifications on the server, your browser could choose to execute a preflight request — an OPTIONS request — before running the actual request. The objective is to discern whether the server would consent to the CORS request based on the response headers.

Should the server's response to the preflight request be void of the mandatory 'Access-Control-Allow-Methods' or 'Access-Control-Allow-Headers' headers, it might result in a fetch request similar to this:

fetch('https://my-api.com/resource', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer my-token',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ message: 'Testing CORS' })
});

In this scenario, the absence of 'Authorization' and 'Content-Type' from the server's 'Access-Control-Allow-Headers' response will cause a failure of the preflight request.

Solution: To tackle this issue, follow this approach:

  1. Server-side: Implement your server to respond to OPTIONS requests with suitable 'Access-Control-Allow-Methods' and 'Access-Control-Allow-Headers' headers. Remember, multiple headers should be added as comma-separated values.
  2. Client-side: Apply in-depth debugging or utilize server logs for more information on the cause of the error. Discerning the cause would help in rectifying the fault effectively.

However, it's crucial to remember that server-side changes might not always be an option if you don't maintain the server you're trying to connect to.

Inspecting Server Responses

Server's Role in CORS

In the Cross-Origin Resource Sharing (CORS) mechanism, the server plays a vital role. It directly influences the treatment of cross-origin requests and defines access levels. The server accomplishes this through a series of HTTP response headers, particularly the Access-Control-Allow-Origin header amongst others.

Consider a typical CORS process initiated by a JavaScript function on the client-side making a fetch request to a server. The browser automatically sets an Origin header with the URL from where the fetch request originates and sends the request to the server.

// JavaScript code on the client side
function fetchData(url) {
    fetch(url).then(response => {
        // Process the server response
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json(); // Returns a promise
    }).then(data => {
        // processData is a function to handle received data
        processData(data);
    }).catch(error => {
        console.error('Error:', error);
    });
}

Upon receiving this request, the server checks the Origin header. If the server decides to permit the request based on its defined rules or a list of allowed origins, it includes an Access-Control-Allow-Origin header in the response.

// Server-side script with CORS configuration
app.use((req, res, next) => {
    const allowedOrigins = ['http://localhost:3000', 'http://example2.com'];
    const origin = req.headers.origin;
    if (allowedOrigins.includes(origin)) {
       res.header('Access-Control-Allow-Origin', origin);
    }
    next();
});

Note: The server script above is written in Express.js framework syntax and demonstrates how to allow a list of specific origins.

Inspecting the Server Response

Once the browser receives the server's response, it checks the value of the Access-Control-Allow-Origin header. If this value matches the origin of the page or is a wildcard ('*'), the JavaScript code is allowed to access the response. If not, the browser blocks the response and throws a 'CORS error', indicating that the Access-Control-Allow-Origin value from the server disallows the request from the provided origin.

This mechanism ensures servers cannot arbitrarily infract the client's security policy. Without a fitting Access-Control-Allow-Origin header in the server's response, JavaScript doesn't receive the response data and can't subsequently use or display it.

By default, JavaScript can only access certain "safe" response headers like Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, and Pragma. For JavaScript to access any other response headers, the server must include an Access-Control-Expose-Headers header, specifying a comma-separated list of other headers that can be accessed.

// Server-side script with more headers
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Expose-Headers', 'API-Key, Content-Encoding');
    next();
});

Therefore, a deep understanding of the server's role and specific response headers in CORS is cardinal to appropriately managing cross-origin requests. By modifying server responses, developers can regulate the projects—their servers grant to specific or all origins and manage what headers their JavaScript code can access.

Thought-provoking questions to consider:

  1. How would the application's behavior and security be impacted if 'Access-Control-Allow-Origin' changed from '*' to a specific origin?

  2. What potential problems might you encounter when exposing unsafe headers? How can you mitigate them?

  3. How would you debug a CORS issue resulting from improper server response headers?

  4. How would you handle the 'CORS Errors' thrown by the browser when the Access-Control-Allow-Origin value is not valid?

Best Practices and Security Considerations

Best Practices for Fetch with CORS

When you work with the fetch() function to handle Cross-Origin Resource Sharing (CORS), you are bound to observe certain best practices that guarantee efficient, safe, and consistent request executions.

Firstly, always specify the mode in a fetch() call as cors. This is important to ensure that fetch requests are treated as CORS requests. Omitting the mode property or setting it to cors implicitly sets credentials to omit, which avoids sending any cookies with the request.

fetch(url, {
    method: 'GET',
    mode: 'cors', //set the request mode as cors 
})
.then(response => response.json())
.catch(error => console.log(error));

Another important practice revolves around handling credentials. If you need to send credentials such as cookies or HTTP authentication with your request, set credentials: 'include'. However, please note that this should only be done if it is absolutely necessary for your use case as it potentially opens doors for security risks.

fetch(url, {
    method: 'GET',
    mode: 'cors',
    credentials: 'include', 
})
.then(response => response.json())
.catch(error => console.log(error));

Unpacking Security Considerations

While fetch provides you with greater control and flexibility in handling network requests, being aware of the associated security considerations is critical.

Perhaps the most significant security layout is the Cross-Site Scripting (XSS) vulnerability. This type of attack occurs if you inadvertently interpret the fetched data as code. To prevent this, never use the fetched data in a way that it gets interpreted as code. In specific terms, avoid dangerous APIs such as innerHTML. For instance:

// Do NOT do this
document.querySelector('#my-div').innerHTML = fetchedData;

In most cases, it makes sense to parse the response as JSON. This will allow you to safely access the properties in the object. Make sure you include error handling for the parsing stage as bad JSON can lead to an exception being thrown.

fetch(url, {
    method: 'GET',
    mode: 'cors'
})
.then(response => response.json())
.then(data => console.log(data)) // Safely handle data
.catch(error => console.log(error)); // Handle errors

Closing Thoughts

When used effectively, the fetch API provides powerful capabilities to modern JavaScript applications, allowing them to make cross-origin requests seamlessly. However, as with all powerful tools, it is important to use fetch responsibly by following best practices and always consider the security implications of your code. Always remember to use the appropriate mode, handle credentials carefully, and take steps to avoid potential XSS vulnerabilities.

Summary

In this article, the author explores the topic of making cross-origin requests with the Fetch API in modern web development. They start by explaining the concept of cross-origin requests and the restrictions imposed by the same-origin policy for web security. The article then delves into the intricacies of CORS (Cross-Origin Resource Sharing), preflight requests, and the role of HTTP headers in managing cross-origin requests with the Fetch API. The author also discusses common CORS errors and provides solutions to handle them effectively.

Key takeaways from the article include understanding the importance of managing cross-origin requests for secure web development, the role of the server in determining access control with CORS headers, and best practices for using the Fetch API with CORS.

One challenging technical task that the reader can try is to implement a server-side configuration to handle cross-origin requests and include the necessary CORS headers. This task would require the reader to understand the different response headers that need to be set, such as Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers, and configure them correctly based on their specific use case. By successfully completing this task, the reader would gain a deeper understanding of how to manage cross-origin requests and ensure secure and efficient communication between browsers and servers.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers