When firing an API request from Lightning Web Components (LWC), have you ever run into errors like “Refused to connect because it violates the document’s Content Security Policy” or “Access has been blocked by CORS policy”? In this blog post, we’ll explore the reason behind these errors and how to fix them.

The same-origin policy

Modern web browsers have built-in security mechanisms that provide an added layer of security to your web applications, at the heart of which is the same-origin policy (SOP).

The same-origin policy prevents scripts on pages served by one origin (e.g., codey.com) from accessing resources, such as DOM elements of an Iframe or APIs from another origin (e.g., astro.com). The origin of a request is the domain name of the page sending the request, which is determined when the page is first loaded into the browser. All subsequent requests are checked against this domain name, and only if the request is to the same domain can you access the response. This helps reduce a few types of Cross Site Request Forgery (CSRF) and Cross Site Scripting (XSS) attacks. The origin of requests fired from LWC components embedded in Lightning Experience is typically <mydomain>.lightning.force.com.

Below is an example of requests governed by the same-origin policy. This sequence diagram shows a browser making a GET request to codey.com for first page load. Codey.com responds with index.html, so the origin for the page is therefore codey.com. The browser then makes a GET call to codey.com/api/getData, which successfully returns a response. The browser then makes a cross-origin API call to astro.com/api/getData that is not allowed by SOP.
A sequence diagram that shows SOP preventing a cross-origin API request

There are two important things to remember about the same-origin policy:

  • First, it is only applicable for requests made via JavaScript code in a web browser. Requests made outside the web browser, such as curl, server-to-server communication, or non-script requests from the browser (like HTML form submits, embedding external images, and scripts in an HTML page) aren’t bound by the same-origin policy.
  • Second, this policy doesn’t prevent a request from being made. It just prevents the script from accessing the response.

So, how do we prevent a browser from making a request to another origin in the first place, and how do we allow the scripts to read the response to a request to another origin? That’s where the Content Security Policy (CSP) and Cross-Origin Resource Sharing (CORS) come into the picture. CSP defines what data can be loaded on a page, and CORS defines what data other pages can load from it.

SOP, CSP, and CORS together give an additional layer of security to API requests by defining what requests can be sent by a browser, and what responses can be read by the browser. Let’s dive a little deeper.

Content Security Policy

Content Security Policy (CSP) prevents a website itself from loading content from a third party (i.e., a different origin). This is defined either via the Content-Security-Policy HTTP header or by using the HTML meta tag <meta http-equiv="Content-Security-Policy">. You can use different policy directives to control which domains different resources can be loaded from. For example, to specify that images can only be loaded from an Amazon S3 bucket, and API calls can only be made to myapi.astro.com, you can define the directive below:

Don’t forget to add self to the directives, because it may prevent the site from loading resources from itself (i.e., the same origin). These headers must be set by the server during the first page load. Similar to SOP, CSP is relevant only for requests made from a browser.

Here is how the previous example would look when a CSP header that only allows calls to self and astro.com is added to the web page. CSP would prevent requests to an endpoint on ruth.com. This example doesn’t show the responses to the requests fired.
A sequence diagram that shows a request that is prevented from being fired due to CSP

Enforcing CSP in LWC

Every page in Lightning Experience has default CSP headers. For example, one of the directives included is script-src 'self', which ensures that only scripts from the same origin can be called. This is the reason why you need to upload third-party scripts as Static resources to use them in LWC. Static resources in Lightning Experience are served from the lightning.force.com domain, which is the same as a Lightning Experience page.

Many other Salesforce domains and subdomains like https://static.lightning.force.com, *.visualforce.com, and https://<mydomain>--c.documentforce.com are also safe-listed under different directives.

Here is a screenshot with the complete list of CSP directives used on a Lightning page.
The Network tab of Chrome developer tools showing the various CSP directives present on a Lightning page

When making an API callout to a third-party endpoint from LWC, you can add its domain to CSP Trusted Sites from Salesforce Setup and ensure that the connect-src checkbox is checked. This ensures that this domain is automatically added to the connect-src directive.

Without this, a fetch() call in LWC fails, and the error object has the message “Failed to fetch".

The error object doesn’t have any information about the CSP failure. It is only logged in the browser console.

The Console tab of Chrome developer tools showing a CSP error and a console log

Cross-Origin Resource Sharing

While CSP can be used to allow a website to make a request to a safe-listed third party, it is up to the third party to allow a given origin to read the response to the request. This is enforced using Cross-Origin Resource Sharing (CORS).

CORS is a way to relax SOP. CORS allows a server to define which origins a particular resource is allowed to be accessed from. It is enforced by the browser via the Origin and Access-Control-Allow-Origin HTTP headers.

The Origin header is sent by web browsers along with a request that indicates where the request has come from. Non-browser requests typically don’t include the Origin header.

The Access-Control-Allow-Origin header is sent by the server along with the response that indicates which origin should be able to access the response.

A value of * denotes that any origin can access the response.

Once a response is received by a browser, it checks for the Access-Control-Allow-Origin header on the response. If it is present, and the value is either the current origin or is *, the response is allowed to be accessed by the script. If not, the request fails with a CORS error. A CORS error prevents your script from accessing the response of the request.

Similar to SOP, CORS is also enforced by a web browser and is not applicable to the requests made outside the web browser.

CORS works slightly differently for different types of API requests. The requests from a browser can be of two types: simple requests and preflighted requests. The browser automatically determines the type of request based on the Request method, Content Types, and Headers.

Simple requests

In the case of simple requests, the actual HTTP request is immediately fired. Here is an example of a successful simple request. The server checks to see if the incoming request’s origin is safe-listed. If yes, it processes the request and returns a response along with the Access-Control-Allow-Origin header containing the origin’s domain. The browser then allows the script to read the response.
This sequence diagram showing a successful simple request

Below is an example of a simple request that fails with a CORS error. It is important to note that, just because you see a CORS error, it doesn’t mean that the request isn’t sent. In fact, the request is sent, and because of the missing Access-Control-Allow-Origin header on the response, you see a CORS error, and your script is denied access to the response. Interestingly, whether or not certain action happens on the server as a result of your request is totally dependent on the API implementation. For example, some APIs might not execute their business logic because the origin check fails, while some APIs might execute actions, and instead, just omit the Access-Control header in the response. You may notice this behavior most often with fire-and-forget requests.
A sequence diagram showing a simple request that fails with a CORS error due to missing Access Control header

Preflighted requests

In the case of preflighted requests, a “CORS Preflight” request is first sent to the server to determine if the actual request will be allowed (i.e., if the Origin, Methods, etc., are allowed). Only if the preflight request is successful will the actual request be sent.

Here is an example of a successful preflighted request.
A sequence diagram showing a successful preflight request

Here is an example of a failed preflight request. The preflight request fails because the value of the Access-Control-Allow-Origin header returned by astro.com doesn’t match the origin of the request. There is also a mismatch in the values of the Access-Control-Request-Method header of the request and the Access-Control-Allow-Methods header returned by astro.com. Since the preflight request has failed, the actual request is never sent to the server. So no actions on the server happen as a result of your request.
A sequence diagram showing a failed preflight request and how the actual request is never fired

Working with CORS in LWC

While CSP is within our control (we can define what resources Salesforce can load), CORS is controlled by the service provider (i.e., the owner of the third-party endpoint).

Do you own the external API? If you own the external API, you will have to make changes to ensure that the Access-Control-Allow-Origin header is present on the responses from your API. For example, if you are using the express framework, you can use the cors npm package to enable CORS.

But keep in mind that non-browser requests don’t include the Origin header, so make sure that you handle those cases as well. If not, your API might not work when not called from a browser.

If you don’t own the external API… Depending on your use case and how the API is implemented, you can either ignore the CORS error, suppress it, or bypass it completely.

Ignoring CORS errors
You can ignore a CORS error if you know for a fact that the API will receive and act on your request, and you are not interested in the response (e.g., fire-and-forget requests). But keep in mind, the then() block of your fetch call is never invoked when a CORS error occurs. This poses two problems.

First, your script will never know if your request was successful. The response object typically includes a status property that contains the HTTP status codes of the response. Since the then() block is never executed, you will never be able to read the response object, and in turn, its status property. So, if your request returns a 401 error, there is no way for your script to know that. However, you can see it in the Network tab of your browser.

Second, you won’t be able to run business logic that is dependent on the status of the response. But if you have business logic that needs to run whether or not your callout was successful, you will have to put it in the finally block.

Both these problems is why ignoring CORS errors is generally discouraged.

Suppressing CORS errors using no-cors
When you use the no-cors option when firing a fetch request, the request will not fail with the CORS error. That is, you will not see the CORS error in the browser console anymore, and the then() block is executed. But similar to ignoring CORS errors, there are major caveats to using the no-cors mode.

First, while the then() block is executed, and you now have access to the response object, the type of response you get is “opaque.” This means that you will not be able to see the response body or headers. The status also has the value 0, instead of regular HTTP status codes, and the ok property of the response is set to false irrespective of the actual status.
A screenshot of an opaque response object logged in the browser console that shows the ok property to be false, body to be null, status to be 0, and headers to be empty
You will need to handle this type of response in the code accordingly, for example, by using the response.ok flag.

Second, and most importantly, the no-cors mode also strips any unsafe headers, such as the Authorization header, from the request. It only allows HEAD, GET,or POST methods, and it also converts the content type of your POST request body to application/x-www-form-urlencoded, multipart/form-data, or text/plain. So, in case you are hitting an endpoint that needs Authentication via the Authorization header, then firing it in no-cors mode will result in an authentication error. The auto conversion of the content-type of your request might also result in a malformed request.

But no-cors is not all bad. If a cross-origin GET or HEAD request is made in no-cors mode, the Origin header will not be added, and it also prevents a preflight request and directly hits the API endpoint. This can ensure that APIs process the request like a non-browser request instead of rejecting the request due to origin mismatch (based on their implementation, as discussed earlier in this post).

If the no-cors mode isn’t a fit for your use case because of the above caveats, the only option left is to use a proxy between LWC and the third-party API.

Bypass CORS errors using a proxy: Apex or Heroku
As mentioned earlier, CORS is enforced only by the browser. So, any workaround that doesn’t involve a browser means that you’ve bypassed CORS. In general, it is more secure to make callouts from the server side because the JavaScript source code of a page is easily accessible from a browser, and this exposes any secrets or tokens that you may have used in your code when hitting an endpoint.

In Salesforce, the easiest way to make callouts from the server side is to use Apex. You can then call this Apex class from LWC. Check out this example to see it in action. But if you aren’t an Apex developer, you can also create a Node.js proxy on Heroku that makes the API call on your behalf and adds CORS headers to the response before sending it to LWC. You will have to ensure that you are adding sufficient security to the proxy to make sure you don’t open the door to other kinds of attacks.

What about Salesforce APIs?
You can’t make calls to Salesforce APIs from LWC hosted on Salesforce. You will either have to use lightning/ui*Api Wire Adapters or use Apex classes to make those callouts. But when you are making calls to Salesforce APIs via JavaScript or LWC hosted on external systems, you can add the external system’s domain to your Salesforce CORS allowlist.

If a request includes the safe-listed origin, Salesforce returns the origin in the Access-Control-Allow-Origin HTTP header along with any additional CORS HTTP headers. If the origin isn’t included in the safelist, Salesforce returns the HTTP status code 403. Salesforce uses this status code for different types of errors, such as insufficient permissions, so you’ll want to check the response’s status message to verify if it indeed is a CORS error.

Summary

While CSP errors when calling APIs from LWC are easy to fix because you control what can be loaded on your web page, CORS errors need much more analysis to figure out whether to ignore, suppress, or bypass them. While ignoring the errors and suppressing errors using the no-cors mode works for a few use cases, you might need to create Apex or Heroku proxies to bypass most CORS errors.

Here are a few resources to help you dive deeper into these web security concepts:

About the author

Aditya Naag Topalli is a 14x Certified Lead Developer Advocate at Salesforce. He empowers and inspires developers in and outside the Salesforce ecosystem through his videos, webinars, blog posts, and open source contributions, and he also frequently speaks at conferences and events all around the world. Follow him on Twitter or LinkedIn and check out his contributions on GitHub.

Get the latest Salesforce Developer blog posts and podcast episodes via Slack or RSS.

Add to Slack Subscribe to RSS