Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

CORS for a frontend

A frontend often runs on one address, for example http://localhost:5173 (the Vite dev server), and the API on another, http://localhost:8080. By default a browser does not let JavaScript from a page of one origin (scheme, domain and port) read responses from another. The mechanism that lets a server allow such requests is called CORS (Cross-Origin Resource Sharing). Without it, fetch("http://localhost:8080/posts") on a frontend page fails with an error in the browser console.

How CORS works

  1. The browser adds an Origin header with the page address to the request.
  2. For "non-simple" requests, such as POST with JSON or with an Authorization header, the browser first sends a preflight request with the OPTIONS method.
  3. The server answers with an Access-Control-Allow-Origin header. If the origin is not there, the browser does not give the response to the page.

curl and other programs do not check CORS rules — it is a protection of the browser.

The CORS plugin

Javalin has a bundled plugin. Enable it in the configuration:

// src/main/java/com/example/blog/Main.java
config.bundledPlugins.enableCors(cors -> {
    cors.addRule(rule -> rule.allowHost("http://localhost:5173"));
});

addRule adds a rule, allowHost lists the allowed origins. The plugin answers OPTIONS requests by itself and adds the needed headers.

Checking it

Let us pretend to be a browser and send an Origin header.

curl -i -H "Origin: http://localhost:5173" http://localhost:8080/posts
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: http://localhost:5173
Vary: Origin

[{"id":1,"title":"Hello, H2","body":"Posts now live in a database.","author":"Oleg"}]

Now the preflight request that the browser sends before a POST with a password:

curl -i -X OPTIONS http://localhost:8080/posts \
     -H "Origin: http://localhost:5173" \
     -H "Access-Control-Request-Method: POST" \
     -H "Access-Control-Request-Headers: content-type, authorization"
HTTP/1.1 200 OK
Content-Type: text/plain
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Allow-Methods: POST
Access-Control-Allow-Origin: http://localhost:5173
Vary: Origin

The server allowed the method and the headers, and the browser will send the real request.

Other rule settings

SettingWhat it does
rule.allowHost("https://blog.example.com")Allow specific origins
rule.allowHost("*.example.com")Allow all subdomains
rule.anyHost()Allow everyone (the * header)
rule.allowCredentials = trueAllow cookies and the Authorization header in browser requests
rule.exposeHeader("X-Total")Make a response header visible to scripts
rule.path = "posts*"Apply the rule only to some addresses

Common mistake

If the origin is not in the list, the plugin answers with code 400, although the response body is there:

curl -i -H "Origin: http://evil.example" http://localhost:8080/posts
HTTP/1.1 400 Bad Request
Content-Type: application/json

[{"id":1,"title":"Hello, H2","body":"Posts now live in a database.","author":"Oleg"}]

There is no Access-Control-Allow-Origin header, so the browser does not give the response to the page. When you see a CORS error in the browser console, first compare the page address with allowHost down to the port and scheme: http://localhost:5173 and http://127.0.0.1:5173 are different origins.

Thanks for reading!

Donate to the site — we work hard for you.