CORS in REST channels

Preflights, allowed origins and response headers - serving browser applications from any origin.

Browsers enforce the same-origin policy, so a web application served from one origin can call your REST channels on another origin only if the response includes CORS headers. This page explains what Zato handles automatically and how to serve browser applications from any origin.

What is built in

Every REST channel handles CORS automatically for a fixed set of origins:

  • https://zato.io
  • http://localhost and http://127.0.0.1, on any port

For these origins, a preflight OPTIONS request is answered before authentication runs, with status 204 No Content and these headers:

Access-Control-Allow-Origin: <the origin echoed back>
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: X-API-Key, Authorization, Content-Type
Access-Control-Max-Age: 86400

On the actual request that follows, Access-Control-Allow-Origin is added to the response. The origin is always echoed back individually, never *.

This means local development against a Zato environment works out of the box - a frontend running on http://localhost:3000 can call your channels with no CORS configuration anywhere.

Serve your own origins

For requests from any other origin, the built-in handling does not add headers and your service sets them itself, through self.response.headers:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetCustomer(Service):
    name = 'demo.rest.get-customer'

    def handle(self) -> 'None':

        # The origin your frontend is served from
        allowed_origin = 'https://app.example.com'

        self.response.headers['Access-Control-Allow-Origin'] = allowed_origin
        self.response.payload = {'customer_id': 123, 'name': 'Alexis Martin'}

If the browser sends a preflight first - which it does for anything beyond simple GET and POST requests, and always when custom headers such as X-API-Key are involved - the OPTIONS request reaches the channel like any other request. Handle it with a handle_OPTIONS method:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetCustomer(Service):
    name = 'demo.rest.get-customer'

    def _add_cors_headers(self) -> 'None':
        self.response.headers['Access-Control-Allow-Origin'] = 'https://app.example.com'
        self.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
        self.response.headers['Access-Control-Allow-Headers'] = 'X-API-Key, Content-Type'
        self.response.headers['Access-Control-Max-Age'] = '86400'

    def handle_OPTIONS(self) -> 'None':
        self._add_cors_headers()

    def handle_GET(self) -> 'None':
        self._add_cors_headers()
        self.response.payload = {'customer_id': 123, 'name': 'Alexis Martin'}

Preflights sent by browsers never include credentials, so a channel whose service answers its own preflights cannot require security on the OPTIONS method. Assign no security definition to such a channel and authenticate the actual requests in the service, or use two channels, one without security for OPTIONS and one with security for the remaining methods, both pointing to the same URL path.

See also

PageWhat it covers
REST channelsCreating the channels that browser applications call
HTTP verbsThe handle_OPTIONS and other verb-specific handlers
AuthenticationThe security definitions that preflight requests interact with

Learn more