Clojure and Cross Origin Resource Sharing (CORS)

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.