Clojure and Cross Origin Resource Sharing (CORS)

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8000/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.
If you're dealing with the above error with your Clojure web app you've come to the right place. This is a common problem when fetching data from a different origin (read URL) for single-page web applications (SPA). A common scenario is that an SPA is served from AWS S3 and it is fetching data from different servers.
How to Fix "The Same Origin Policy disallows reading the remote resource"
A common fix is to add a backend middleware to deal with the CORS headers such as ring-cors
and this is what this post is about. Alternatively, you can serve the web app from the API's origin if possible or write your logic (i.e. middleware) to handle the CORS requests or if you're dealing with a third-party API there is likely an option to configure your allowed web origins.
Configure CORS Middleware
First, add the latest version of ring-cors to your dependencies.
{:paths ["src" "resources"]
:deps {org.clojure/clojure {:mvn/version "1.11.1"}
ring/ring-core {:mvn/version "1.6.3"}
ring/ring-jetty-adapter {:mvn/version "1.6.3"}
ring-cors/ring-cors {:mvn/version "0.1.13"}}}
Require the dependency and wrap the application handler with ring.middleware.cors/wrap.cors
.
(ns tvaisanen.cors
(:require [ring.middleware.cors :refer [wrap-cors]]
[ring.adapter.jetty :as jetty]))
(defn app [_request]
{:status 200
:headers {"Content-Type" "text/html"}
:body "OK"})
(defonce server (atom nil))
(defn start! []
(reset! server
(jetty/run-jetty
(wrap-cors #'app
:access-control-allow-origin #"https://tvaisanen.com"
:access-control-allow-methods [:get :put :post :delete])
{:port 8000 :join? false})))
(comment
(.stop @server)
(start!))
Let's run the server and validate that it is working as expected.
Verify the Configuration
Verify the configuration by making an HTTP request from the browser console and inspecting the headers from the network tab.
Send the Request
Type this in your browser's dev tools console (from the origin configured for CORS).
fetch("http://localhost:8000").then(console.log)
Request Headers
You should see something similar to this in your request headers when you inspect the network tab.
GET / HTTP/1.1
Host: localhost:8000
Origin: https://tvaisanen.com
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
Response Headers
And the response headers are similar to this.
HTTP/1.1 200 OK
Date: Thu, 19 Oct 2023 05:53:16 GMT
Content-Type: text/html
Access-Control-Allow-Methods: DELETE, GET, POST, PUT
Access-Control-Allow-Origin: https://tvaisanen.com
Transfer-Encoding: chunked
Server: Jetty(9.2.21.v20170120)
What is important to notice here is that the response header Access-Control-Allow-Origin
has the value from the requests Origin
header. Having this with the Access-Control-Allow-Methods
header tells the browser that it is okay to use the data they are asking for.
Let's double-check that this is indeed what is happening from the command line.
CORS Headers Included
When the origin header is passed and the value matches the one that is configured on the server side we get the expected Access-Control-Headers
.
❯ http localhost:8000 'origin:https://tvaisanen.com'
HTTP/1.1 200 OK
Access-Control-Allow-Methods: DELETE, GET, POST, PUT
Access-Control-Allow-Origin: https://tvaisanen.com
Content-Type: text/html
Date: Thu, 19 Oct 2023 06:02:10 GMT
Server: Jetty(9.2.21.v20170120)
Transfer-Encoding: chunked
OK
CORS Headers Missing
If the value of the origin header does not match the configured value the access control headers should not be in the response.
❯ http localhost:8000 'origin:https://not-tvaisanen.com'
HTTP/1.1 200 OK
Content-Type: text/html
Date: Thu, 19 Oct 2023 06:02:18 GMT
Server: Jetty(9.2.21.v20170120)
Transfer-Encoding: chunked
OK
Looks like everything is working as expected!
There's more to cross-origin resource sharing but this should be enough to get you over the initial problem. I recommend reading related MDN Docs to learn more about the topic.
Thanks again for reading, I hope you found this useful.
Subscribe to my newsletter
Read articles from Toni Väisänen directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Toni Väisänen
Toni Väisänen
Software engineer @ Metosin Ltd Need help with a project, contact: first.last@metosin.com As a 𝐜𝐨𝐧𝐬𝐮𝐥𝐭𝐚𝐧𝐭, I help clients find technical solutions to their business problems and facilitate communication between the stakeholders and the technical team. As a 𝐟𝐮𝐥𝐥-𝐬𝐭𝐚𝐜𝐤 𝐝𝐞𝐯𝐞𝐥𝐨𝐩𝐞𝐫, I build technical solutions for client's problems from user interfaces, and backend services to infrastructure-as-code solutions. As a 𝐦𝐚𝐜𝐡𝐢𝐧𝐞 𝐥𝐞𝐚𝐫𝐧𝐢𝐧𝐠 𝐞𝐧𝐠𝐢𝐧𝐞𝐞𝐫, I create, validate and deploy predictive models.