Cross-Origin HTTP Requests

Cross-Origin Request with Preflight

A more complex CORS scenario necessitates a preliminary HTTP message exchange: such "preflight negotiation" is required for the client and the server to agree upon the set of allowed methods and headers.

In the code below, the client script not only tries to retrieve a resource from an extraneous domain, but also appends custom headers to the HTTP request:

var xhr = new XMLHttpRequest();
xhr.onload = loadEvent => {
 console.log(loadEvent.target.responseText);
 console.log(loadEvent.target.getAllResponseHeaders());
};
xhr.open('GET', 'http://example.net/cross-origin-request-handler.php');
xhr.responseType = 'text';
xhr.setRequestHeader('X-Header-1', 'Value 1');
xhr.setRequestHeader('X-Header-2', 'Value 2');
xhr.send();

The browser does not send the GET request directly: first it ascertains the server's consent to accept custom headers of the cross-origin request. To perform the "lookup", the browser creates a preflight request with the OPTIONS method and provides it with two special headers - Access-Control-Request-Method and Access-Control-Request-Headers:

request line
OPTIONS http://example.net/cross-origin-request-handler.php HTTP/1.1
request headers
Origin: http://example.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: x-header-1, x-header-2

The Access-Control-Request-Method indicates the HTTP method which the upcoming CORS request is going to use. Access-Control-Request-Headers is a list of comma-separated headers of the request.

If the server agrees to handle future CORS requests with the specified method and custom headers, it can respond to the preflight lookup in this way:

<?php
 header('Access-Control-Allow-Origin: *');
 header('Access-Control-Allow-Methods: GET');
 header('Access-Control-Allow-Headers: x-header-1, x-header-2');
?>

In addition to Access-Control-Allow-Methods and Access-Control-Allow-Headers, the server can include the Access-Control-Max-Age in its response. The header will tell the browser to abide by the result of the current preflight negotiation for a specified number of seconds:

header('Access-Control-Max-Age: 600');

Another hint for the browser is the Access-Control-Allow-Credentials header. Its true value gives permission to the use of forthcoming CORS requests with user credentials:

header('Access-Control-Allow-Credentials: true');

After the browser parses the server response to the preflight request, it generates a real GET request with custom headers created in the client code. The CORS message contains the Origin header, anyway:

request line
GET http://example.net/cross-origin-request-handler.php HTTP/1.1
request headers
Origin: http://example.com
X-Header-1: Value 1
X-Header-2: Value 2

The server must include Access-Control-Allow-Origin in its response. In addition, it can enumerate response headers that will be available to the client script:

<?php
 header('Access-Control-Allow-Origin: *');
 header('Access-Control-Expose-Headers: date, server, transfer-encoding');
 header('Content-Type: text/plain; charset=UTF-8');
 echo 'This is just a demo';
?>

If the browser does not detect the Access-Control-Expose-Headers in the server response, it only exposes the Content-Type header to the script. An attempt to get the value of some other header will return null.

For illustration, the server-side code below handles both preflight OPTIONS and cross-origin GET requests in a single block of code:

<?php
 if($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { // handling preflight request
  header('Access-Control-Allow-Origin: *');
  header('Access-Control-Allow-Methods: GET');
  header('Access-Control-Allow-Headers: x-header-1, x-header-2');
  header('Access-Control-Max-Age: 600');
 } else { // handling CORS requests
  header('Access-Control-Allow-Origin: *');
  header('Access-Control-Expose-Headers: date, server, transfer-encoding');
  header('Content-Type: text/plain; charset=UTF-8');
  echo 'This is just a demo';
 }
?>

CORS and Fetch API

The same principles of cross-origin requests handling are applied to routines based on the Fetch API:

var request = new Request('http://example.net/cross-origin-request-handler.php', {
 method: 'GET',
 headers: {
  'X-Header-1': 'Value 1',
  'X-Header-2': 'Value 2'
 },
 mode: 'cors'
}
);
fetch(request).then(response => {
 console.log(response.headers.get('Connection')); // null
 console.log(response.headers.get('Date')); // a valid value
 response.text().then(txt => {
  console.log(txt);
 });
}, eObj => {
 console.error(eObj);
}
);