CORS in GCP API Gateway

CORS in GCP API Gateway

If you're like me, building an application with Backend APIs protected behind a Google Cloud API Gateway, you've probably encountered a common issue: The ability to handle CORS preflight requests from your FrontEnd!

This is less of an issue with the self-administered ESP and ESPv2 offerings, but using the Managed API Gateway offering, you relinquish control of startup options which require some careful considerations for scenarios such as handling CORS preflight requests.

Background

I'm lazy, so here's Gemini Pro 2.5's explanation of CORS:

CORS (Cross-Origin Resource Sharing) preflight OPTIONS requests are a security mechanism used by web browsers to check with the server if the actual request (e.g., POST, PUT, DELETE, or a GET with custom headers) is safe to send.
Their primary purpose is to ensure that a server explicitly permits cross-origin requests that could have side effects on the server's data. This prevents malicious websites from making requests to an API on another domain without the API's explicit permission, enhancing web security.

Now that we've got that out of the way, let's get into issues with GCP and how to handle this.

OpenAPI Spec

I know AI is all the buzz right now, but this is not to be confused with OpenAI!

Configuration of a GCP API gateway required an OpenAPI Spec YAML file: https://cloud.google.com/api-gateway/docs/openapi-overview

An example of a simple OpenAPI Spec file for handling backend routes is:

    swagger: "2.0"
    info:
      title: API_ID optional-string
      description: "Get the name of an airport from its three-letter IATA code."
      version: "1.0.0"
    host: DNS_NAME_OF_DEPLOYED_API
    schemes:
      - "https"
    paths:
      "/airportName":
        get:
          description: "Get the airport name for a given IATA code."
          operationId: "airportName"
          parameters:
            -
              name: iataCode
              in: query
              required: true
              type: string
          responses:
            200:
              description: "Success."
              schema:
                type: string
            400:
              description: "The IATA code is invalid or missing."

Here you can see we define a single /airportName path which accepts GET requests, requiring parameters and responding with either 200 for success or 400 for an invalid or missing IATA code.

Problem

As is, the above spec will work perfectly fine for direct requests to the Gateway, assuming the backend is properly configured of course. Direct requests through Curl or a tool such as Postman will respond appropriately. The issue arises when attempting to access these endpoints using a Front End application in a Browser such as Google Chrome. As we figured out above, the browser will send a HTTP OPTIONS request to see if the actual request is safe to send and if the origin is accepted.

We need to inform the GCP API Gateway that it should handle these CORS Preflight Options requests otherwise it will result in all requests to this API Gateway from the frontend failing with a CORS error HTTP 503 "No Healthy Upstream".

We have two options:

  1. Define an explicit OPTIONS method on every endpoint path that will be accepting requests, which will forward these to the backend on every path.
  2. Tell the API Gateway to automatically forward all OPTIONS requests to the backend and handle them there.

The second option makes the most sense and is specifically designed for this use-case, otherwise any change to endpoint paths, adding or removing, will need a duplicated block of code to handle those OPTIONS requests. Not very DRY-Programming.

Implementation

For this we'll need two aspects.

  1. Firstly we'll need to tell the API Gateway to handle all OPTIONS requests for defined endpoints.
  2. Secondly, we'll need to ensure our Backend (in this example, a FastAPI Application) can handle all of these.

Let's tackle the first part. For this, you'll need a few variables:

  • API Gateway managed service name - You can get this from the GCP Console. It will look something like {api_name}-ID.apigateway.{project-name}.cloud.goog
  • Your backend URL - Where the actual defined endpoints are hosted.

The below spec is using the example of :

  • demo-gateway-18bxrikyi5dy4.apigateway.project.cloud.goog as the Managed Service name and
  • my-cool-host.highoncloud.co.uk as the backend URL
# openapi-spec.yaml
swagger: "2.0"
info:
  title: "Voqu VBaaS Backend API"
  description: "API for the Voicebot-as-a-Service platform"
  version: "1.0.0"
schemes:
  - "https"
produces:
  - "application/json"

host: "demo-gateway-18bxrikyi5dy4.apigateway.project.cloud.goog"
x-google-endpoints:
  - name: "demo-gateway-18bxrikyi5dy4.apigateway.project.cloud.goog"
    allowCors: True


# We are returning to the path-based approach, now with the validator exception.
paths:
  /hello:
    get:
      description: "A sample endpoint to test authentication."
      operationId: "hello"
      x-google-backend:
        address: "my-cool-host.highoncloud.co.uk/hello"
      responses:
        "200":
          description: "A successful response"

The key bit to point out here is:

  • allowCors - This is set on the entire API Gateway host so all OPTIONS requests will be forwarded

That's the magical touch. It needs to be defined on the x-google-endpoints where the name parameter is set as your API Gateway Managed Service name.

Now with all of this in place, all requests sent to this API Gateway, from an application running in Chrome, which are preceded with a CORS preflight OPTIONS request, will be forwarded to the x-google-backend.

Optional Backend Config

We also need to ensure the backend can handle these OPTIONS requests. For this scenario, I'm running a FastAPI application in development mode which simplifies things. Simply adding the below block of code to your application main.py or otherwise will allow it to handle these appropriately.

  • Note: The below code is only suitable for development where all origins are accepted. This is UNSECURE and should be changed to only accept the appropriate origins in production.
from fastapi import FastAPI

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Allow all origins for testing
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Conclusion

And there we have it. Handling CORS Preflight requests through a GCP API Gateway is complete.

I've been playing with building an application recently so look out for a blog post on securing your endpoints, through the GPC API Gateway, using JWT tokens and Auth0 as an identity provider.