I’ve been playing with htmx lately. I’m not much for JavaScript for most things but I’ve used intercooler.js in the past and htmx is the successor to it. It’s really nice for someone who wants nothing to do with React or Vue or similar.
However, in the past, I’ve always made the htmx (or intercooler.js) requests within the same application process. However, in my current experiment, I needed to make the request to another process bound to a different port on the same machine. This different process is sufficient to trigger the CORS protections in browsers.
My index.html
looks something like the following:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script src="htmx.min.js" defer></script>
<button hx-get="http://localhost:8085/get/the/message" hx-swap="innerHTML">
Click Me
</button>
</body>
</html>
It’s a basic set of ajax in htmx - when you click the button, it issues a GET
request to the URL
specified by the hx-get
field in the <button ...>
element. In this case, the server listening
on :8085
just returns a string like “the time is now 03:18:25.212347” which is swapped into the
button’s innerHTML
field.
The server itself is a simple Elixir application using the Plug package. Briefly - Plug is an Elixir package for composing web applications with functions. It’s used in frameworks like Phoenix but you can think of it as a middleware component.
The Elixir application just exposes a route at /get/the/message
, servicing the GET
verb.
So I started up the Elixir application and then serve my index.html
using
simple-http-server
. After navigating to the
web page, I get the following error in the Developer Console:
Access to XMLHttpRequest at ‘http://localhost:8085/get/the/message’ from origin ‘http://127.0.0.1:8000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
Now - while I have plenty of experience with HTTP servers, it’s almost purely from the non-Browser perspective. I’ve spent most of my career writing services that make requests to other services or command line tools that make requests to services.
This is my first encounter with CORS though I’m loosely familiar with the idea. After some digging,
I discover my server needs to respond with the appropriate response header - Access-Control-Allow-Origin
.
Cool! Let’s add the header… and… womp womp. It’s still not working.
Looking at the Developer Console, I see an OPTIONS
request that was blocked with the explanation
of:
Response body is not available to scripts (Reason: CORS Missing Allow Origin)
Huh - so I’ve never actually seen the OPTIONS
verb used in practice. Doing
some digging, it looks
like it’s mostly used in browsers which tracks since I don’t do much browser work. After some
groping in the dark, I discover that the OPTIONS
request is a side effect of htmx issuing
Preflight requests to check
if the CORS protocol is understood by the server.
Alright - so based on this, sounds like I need to add support for the OPTIONS
verb in our router
and serve the expected response headers for the OPTIONS
request as well. Easy enough:
options "/get/the/message" do
conn
|> put_resp_header("Access-Control-Allow-Origin", "http://127.0.0.1:8000")
|> put_resp_header("Access-Control-Allow-Headers", "Content-Type, hx-current-url, hx-request")
|> put_resp_header("Access-Control-Allow-Methods", "GET")
|> put_resp_header("Access-Control-Max-Age", "86400")
|> send_resp(204, "{}")
end
This chunk of code adds four headers. It allows the CORS request in the first place from the process
bounding to :8000
, it allows headers to be requested for Content-Type, hx-current-url, hx-request
,
and it permits the GET
verb. Oh - and it sets the Max Age of the access control response to a day.
Let’s test it out and… success!
There’s nothing specific to this about Elixir as the server process. It could be Go or Rust, you’d just need to attach the appropriate response headers and support the right verbs.
Idle thought: I’m not sure if this is something nginx
or similar would’ve handled for me in
some instances. Maybe?
—–Posted on: 2022-05-17