CORS Tester for Rails APIs

Give it the endpoint your front end calls and the origin it calls from. We make the request from our own server, show you the Access-Control headers your app actually sent, and write the rack-cors block that would have sent the right ones.

All tools
Check a Rails endpoint's CORS headers

The request your front end makes

The full URL your JavaScript calls, path included. It has to answer from the public internet, so point this at staging if your Rails app only runs on localhost.
Scheme, host and port of the page making the call. This is the value that belongs in origins in cors.rb.
Comma separated, and every one of them has to appear in the resource's headers: list. Leave it empty if the request sets none of its own; name content-type when you post JSON and authorization when you send a token.

We make a real request to your host, so this can take a few seconds.

The request goes out from our server, not from your browser, and nothing that could change data on your host is ever sent.

Why a Rails app answers with no Access-Control-Allow-Origin

Rack::Cors is middleware you insert yourself, and nothing in Rails puts it there for you - not even rails new --api, which is where most of the confusion starts. With no config/initializers/cors.rb there is no middleware in the stack, so no response carries an Access-Control header and the browser blocks every cross-origin read. Once the file exists, the resource pattern still has to match the request path: a block that opens "/api/*" says nothing about /v1/invoices. The preflight is an OPTIONS request that Rack::Cors answers before routing, so a missing route is never the reason a preflight fails - but missing middleware always is. And an exception raised earlier in the stack returns a response that never reaches Rack::Cors at all, which is why a 500 from a cross-origin call shows up in the browser as a CORS error rather than as the error it is.

Reading each row against config/initializers/cors.rb

Each row above is one header, and each header comes from one directive. Access-Control-Allow-Origin is what origins produces. Allow-Methods comes from methods:, Allow-Headers from headers:, Allow-Credentials from credentials: true, Max-Age from max_age: and Expose-Headers from expose:. Rows marked as failing are the reasons a browser would block the call, and the banner names the first of them because fixing that one often changes the rest - a preflight that never gets a 2xx has nothing else worth reading. Rows marked for information change nothing on their own: a missing Max-Age costs you a preflight per request rather than a blocked request, and a missing Expose-Headers only matters if the client reads a header of yours.

Adapting the generated block before you ship it

Treat the block as a starting point. Where two origins need different rules, give each its own allow block rather than listing both in one - the directives inside an allow apply to every origin in it. Keep resource patterns as narrow as the endpoints you mean to open; "*" opens the whole app, including routes you have not thought about. origins "*" and credentials: true cannot be combined, and rack-cors will tell you so at boot rather than silently. Restart the server after editing, because an initializer is loaded once and not reloaded between requests. Remember that staging and production call from different hosts, so the origin belongs in configuration rather than hard-coded. And if your JavaScript needs to read a response header of your own - a pagination total, a request id - list it in expose:, because without that the browser hides it.

Start creating your next app now