Eric Radman : a Journal

Cross-Origin Requests

Modern web browsers all allow REST interfaces to external services depends on using a protocol Cross-Origin Resource Sharing.

This mode is available by setting the mode attribute of a JavaScript Request object. The following is an example using the Fetch API

async function sct_postData(url, data={}) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    mode: 'cors',
    body: JSON.stringify(data)
  });
  return response.json();
}

Specifying 'no-cors' does not mean relax the protocol, it means don't follow the protocol which might allow external resource.

Uncomprehending Options

The first step to enabling CORS may be to prevent the web framework from sending deprecated headers intended to prevent cross-site scripting. For example to prevent Sinatra from setting the X-Xss-Protection header

disable :protection

(It is also possible that a similar option needs to be removed from a reverse proxy.)

Preflight Rules

Before making a cross-origin request browsers use an OPTIONS request to find out what parameters are permitted

options "*" do
  response.headers["Access-Control-Allow-Origin"] = "*"
  response.headers["Access-Control-Allow-Methods"] = "POST, PATCH"
  response.headers["Access-Control-Allow-Headers"] = "Content-Type"
  200
end

For the example above Content-Type needs to be added to the list of allowed headers since application/json is not one of the types allowed by default.

Enabling a Public Endpoint

The following is a simplified version of the route used in sidecomment.io :

post '/ticket' do
  response.headers["Access-Control-Allow-Origin"] = env['HTTP_ORIGIN']
  response.headers["Access-Control-Allow-Methods"] = "POST"
  content_type :json

  create_ticket JSON.parse(request.body.read)
end

These three headers match the broader allow list defined by the OPTIONS reply. Testing this can be tricky because data returned over the wire is not nessesarily accessible through JavaScript. (Chrome will show "Failed to show response data" in the developer tools window.)

It probably makes sense to provide some error handling that provides a hint as to why the reply is empty

  if (response.body)
    return response.json();
  else
    return {'error': 'empty reply (cross-origin request blocked?)'};

Once these components are lined up you should be able to observe a successful request and response!

Last updated on December 28, 2021