Skip to main content

Configure CORS for your Extend app

Last updated on October 5, 2026

Overview​

Enable Cross-Origin Resource Sharing (CORS) when a browser-based web application on a different domain (for example https://mygame.com) needs to call your Extend REST endpoints.

The Extend sample apps ship with CORS support built in but disabled by default. You turn it on and configure it entirely through environment variables — no code changes and no rebuild. While CORS is disabled, your app behaves exactly as before and adds no CORS headers.

This applies to Extend apps that expose REST endpoints through the gRPC gateway: Extend Service Extension and Extend Event Handler with REST API support.

note

CORS is enforced by the browser, not your app. It only affects JavaScript requests from a web page on a different origin (scheme + host + port). Server-to-server and non-browser clients are unaffected.

info

This page covers CORS for your own Extend app. It is a different setting from the namespace-level Allowed Origins (CORS) configuration in the AGS Admin Portal under Foundations > Tools and Utilities, which controls cross-origin access to AGS platform endpoints. Configuring one has no effect on the other.

Before you begin​

  • A deployed Extend Service Extension, or an Extend Event Handler with REST API support.

  • Your app includes the built-in CORS middleware. To check, look for one of these files in your app's source:

    • pkg/common/cors.go — Go sample apps.
    • gateway/pkg/common/cors.go — C#, Java, and Python sample apps, and Event Handler with REST API support.

    If neither file exists, your app predates the built-in middleware. Follow If your app does not include the CORS middleware instead.

  • Access to your app's Environment Variable Configuration in the AGS Admin Portal.

  • The exact origins that will call your app. https://mygame.com and https://admin.mygame.com are different origins.

Step 1: Set the CORS environment variables​

Add the following as Environment Variable Configuration entries on your Extend app. For the Admin Portal steps, see Extend app environment variables and secrets.

KeyValue
CORS_ENABLEDtrue
CORS_ALLOWED_ORIGINShttps://mygame.com

That is enough for typical GET and POST requests. The app applies sensible defaults for every other setting — see Settings reference.

To allow more than one origin, use a comma-separated list of exact origins:

KeyValue
CORS_ALLOWED_ORIGINShttps://mygame.com,https://admin.mygame.com

To let the browser send cookies or the Authorization header on cross-origin requests, also add CORS_ALLOW_CREDENTIALS with the value true.

Never allow credentials from any origin

Do not combine CORS_ALLOW_CREDENTIALS=true with CORS_ALLOWED_ORIGINS=*. This exact combination is invalid per the CORS specification, so browsers reject it and the request simply fails. More importantly, the intent behind it is dangerous: allowing credentialed requests from any origin lets any website a signed-in user visits call your app as that user and read the response, exposing session cookies and access tokens (a cross-site request forgery, or CSRF, risk). Whenever CORS_ALLOW_CREDENTIALS=true, restrict CORS_ALLOWED_ORIGINS to an explicit list of exact origins you trust.

Step 2: Restart and apply​

Click Restart and Apply on your app's configuration page. The CORS variables are read only at startup, so the new values take effect when the app restarts.

Settings reference​

Each setting is read from the matching environment variable at startup.

Environment variableDefaultDescription
CORS_ENABLEDfalseMaster switch. When false, no CORS headers are added and every other CORS variable is ignored.
CORS_ALLOWED_ORIGINS(empty)Comma-separated exact origins, for example https://mygame.com. Set * to allow any origin (not recommended). If this is empty while CORS_ENABLED=true, the app leaves CORS disabled and logs an error — see Troubleshooting.
CORS_ALLOWED_METHODSGET,POST,PUT,DELETE,PATCH,OPTIONSPermitted HTTP methods.
CORS_ALLOWED_HEADERSContent-Type,AuthorizationRequest headers the browser may send. Add any custom headers your app reads.
CORS_EXPOSE_HEADERS(empty)Response headers browser JavaScript may read.
CORS_ALLOW_CREDENTIALSfalseAllow cookies and Authorization on cross-origin requests. Requires exact origins.
CORS_MAX_AGE600Seconds the browser may cache the preflight response. Browsers cap this regardless (roughly two hours in Chromium).
An empty value is not the same as an unset variable

Setting a variable to an empty value overrides its default rather than falling back to it. For example, adding CORS_ALLOWED_METHODS with an empty value allows no methods at all, instead of the six defaults. To keep a default, leave the variable out of your configuration entirely.

CORS applies to your app's REST and HTTP surface only — the gRPC gateway, including its Swagger UI. The metrics endpoint and the raw gRPC port are unaffected.

Security

Prefer exact origins over *, never enable credentials with a wildcard or unrestricted origin (see the warning above), and trim methods and headers to what your app uses. CORS configuration is your responsibility as the app owner.

Test your configuration​

Check the startup logs first. When CORS is active, your app logs a CORS enabled line at startup listing the allowed origins and whether credentials are allowed. This line only appears when CORS is genuinely adding headers, so its absence tells you the configuration did not take effect — check that before debugging the browser.

Against the deployed app. From a page served on an allowed origin, call the https:// URL of your app, then check the browser's Network tab for the OPTIONS preflight and its Access-Control-Allow-Origin response header:

fetch("https://<your-extend-app-url>/v1/your-endpoint", {
method: "GET",
headers: { "Authorization": "Bearer <token>" },
})
.then((res) => res.json())
.then(console.log);

Locally. Run the app locally and send a preflight with curl — calling localhost directly avoids the ingress that can intercept OPTIONS on the deployed URL:

curl -i -X OPTIONS "http://localhost:8000/v1/your-endpoint" \
-H "Origin: https://mygame.com" \
-H "Access-Control-Request-Method: GET"

A correct response includes Access-Control-Allow-Origin: https://mygame.com. Repeat with a disallowed origin (for example https://evil.com) and confirm that header is absent — that proves your allowlist is enforced.

Troubleshooting​

  • No CORS headers, and the logs show an error about CORS_ALLOWED_ORIGINS being empty: you set CORS_ENABLED=true but listed no origins. The app deliberately leaves CORS disabled in that case, because an empty origin list would otherwise allow every origin. Add the origins you want to allow, or set * to allow all intentionally.
  • No CORS headers and no CORS log lines at all: your app does not include the built-in middleware. See If your app does not include the CORS middleware.
  • CORS error despite correct config: call the https:// URL, not http://. Extend 301-redirects http:// to https://, and browsers do not follow redirects during a preflight OPTIONS.
  • Preflight OPTIONS fails: confirm CORS_ENABLED=true, the origin is in CORS_ALLOWED_ORIGINS, and the method and headers are allowed.
  • curl shows different headers than the browser: the Extend ingress may intercept OPTIONS. Verify with a real browser fetch from an allowed origin.

If your app does not include the CORS middleware​

Follow this section only when the file check in Before you begin found no cors.go — for example an app scaffolded from a sample app before the middleware was added, or an app written from scratch. Adding the middleware yourself gives you the same CORS_* environment variables described above.

Unlike the environment-variable steps, this path changes your app's source code, so you must rebuild and redeploy your app afterward. Redeploying the existing build is not enough.

info

Your Extend app may be written in C#, Java, or Python, but its HTTP entry point — the gRPC gateway — is always written in Go. CORS is an HTTP concern, so you add it in Go, regardless of your app's language. You don't need to write Go — paste the file below as-is.

The steps below use the C#, Java, and Python layout, where the gateway is a separate component in the gateway/ folder. If your app is written in Go, the gateway is the app itself — follow the paths in the right-hand column and use slog in place of logrus:

C# / Java / PythonGo
CORS file (Step 1)gateway/pkg/common/cors.gopkg/common/cors.go
Wiring file (Step 3)gateway/main.gomain.go (repo root)
Run go get in (Step 2)gateway/repo root
Logger*logrus.Logger*slog.Logger

For the Go app, replace the github.com/sirupsen/logrus import in cors.go with log/slog and change the parameter to logger *slog.Logger. The logger.Error(...) and logger.Warn(...) calls work the same with both loggers.

Step 1: Add the CORS middleware file​

Create a new file at gateway/pkg/common/cors.go and paste the following:

// gateway/pkg/common/cors.go
package common

import (
"net/http"
"strings"

"github.com/rs/cors"
"github.com/sirupsen/logrus"
)

// CORSConfig holds the cross-origin resource sharing settings for the
// gRPC-Gateway HTTP entry point. Each field maps to a CORS_* environment
// variable read by LoadCORSConfigFromEnv. CORS is disabled unless Enabled
// is true, so existing deployments are unaffected until you opt in.
type CORSConfig struct {
Enabled bool
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
ExposeHeaders []string
AllowCredentials bool
MaxAge int
}

// Active reports whether CORS is both switched on and actually able to add
// headers, which requires at least one allowed origin.
func (c CORSConfig) Active() bool {
return c.Enabled && len(c.AllowedOrigins) > 0
}

// LoadCORSConfigFromEnv reads the CORS configuration from environment
// variables. CORS is off by default: unless CORS_ENABLED=true the returned
// config is inert and NewCORSMiddleware becomes a passthrough.
func LoadCORSConfigFromEnv() CORSConfig {
return CORSConfig{
Enabled: strings.EqualFold(GetEnv("CORS_ENABLED", "false"), "true"),
AllowedOrigins: splitAndTrim(GetEnv("CORS_ALLOWED_ORIGINS", "")),
AllowedMethods: splitAndTrim(GetEnv("CORS_ALLOWED_METHODS", "GET,POST,PUT,DELETE,PATCH,OPTIONS")),
AllowedHeaders: splitAndTrim(GetEnv("CORS_ALLOWED_HEADERS", "Content-Type,Authorization")),
ExposeHeaders: splitAndTrim(GetEnv("CORS_EXPOSE_HEADERS", "")),
AllowCredentials: strings.EqualFold(GetEnv("CORS_ALLOW_CREDENTIALS", "false"), "true"),
MaxAge: GetEnvInt("CORS_MAX_AGE", 600),
}
}

// NewCORSMiddleware returns an HTTP middleware that applies the given CORS
// config. When CORS is not usable it returns an identity middleware that adds
// no headers. When enabled it delegates to rs/cors, which answers preflight
// OPTIONS requests and sets the Vary header.
func NewCORSMiddleware(cfg CORSConfig, logger *logrus.Logger) func(http.Handler) http.Handler {
passthrough := func(next http.Handler) http.Handler { return next }

if !cfg.Enabled {
return passthrough
}

// rs/cors treats an empty AllowedOrigins list as "allow every origin".
// Arriving there by omission is a misconfiguration rather than an intent,
// so refuse to enable CORS instead of silently exposing the API to any
// origin. Allowing everything stays possible, but only explicitly via
// CORS_ALLOWED_ORIGINS=*.
if len(cfg.AllowedOrigins) == 0 {
logger.Error("CORS_ENABLED=true but CORS_ALLOWED_ORIGINS is empty; leaving CORS disabled because an empty origin list would allow every origin. Set explicit origins (e.g. https://mygame.com), or '*' to allow all intentionally")

return passthrough
}

if cfg.AllowCredentials && containsWildcard(cfg.AllowedOrigins) {
logger.Warn("insecure CORS configuration: CORS_ALLOW_CREDENTIALS=true combined with a wildcard origin '*' is invalid per the CORS spec and will be rejected by browsers; specify exact origins (e.g. https://mygame.com) instead")
}

c := cors.New(cors.Options{
AllowedOrigins: cfg.AllowedOrigins,
AllowedMethods: cfg.AllowedMethods,
AllowedHeaders: cfg.AllowedHeaders,
ExposedHeaders: cfg.ExposeHeaders,
AllowCredentials: cfg.AllowCredentials,
MaxAge: cfg.MaxAge,
})

return c.Handler
}

// splitAndTrim splits a comma-separated list, trims whitespace from each
// item, and drops empty entries.
func splitAndTrim(s string) []string {
if strings.TrimSpace(s) == "" {
return []string{}
}

parts := strings.Split(s, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
if trimmed := strings.TrimSpace(p); trimmed != "" {
result = append(result, trimmed)
}
}

return result
}

// containsWildcard reports whether the origins list contains "*".
func containsWildcard(origins []string) bool {
for _, o := range origins {
if o == "*" {
return true
}
}

return false
}
note

cors.go calls the GetEnv and GetEnvInt helpers from the same package. Most sample apps already define them in pkg/common/utils.go. If your gateway's pkg/common folder does not have them, add them before continuing, or cors.go will not compile.

Step 2: Add the CORS dependency​

The file uses the rs/cors package. From inside the gateway/ folder, run:

go get github.com/rs/cors@v1.11.1

This adds the dependency to your go.mod and go.sum. Commit both files.

Step 3: Wire it into the gateway​

In gateway/main.go, find the newGRPCGatewayHTTPServer function. It builds a loggedMux and serves it as the Handler. Wrap loggedMux with the CORS middleware and serve that instead, so CORS is the outermost layer and browser preflight (OPTIONS) requests are answered before reaching the gateway:

loggedMux := loggingMiddleware(logger, mux)

// Add CORS middleware as the outermost layer so that browser preflight
// (OPTIONS) requests are answered here before reaching the gateway mux.
// It is inert unless CORS_ENABLED=true, so behavior is unchanged by default.
corsConfig := common.LoadCORSConfigFromEnv()
corsHandler := common.NewCORSMiddleware(corsConfig, logger)(loggedMux)
if corsConfig.Active() {
logger.WithFields(logrus.Fields{
"allowedOrigins": corsConfig.AllowedOrigins,
"allowCredentials": corsConfig.AllowCredentials,
}).Info("CORS enabled")
}

return &http.Server{
Addr: addr,
Handler: corsHandler, // was: loggedMux
// ...
}
note

NewCORSMiddleware and LoadCORSConfigFromEnv are in the common package — the package behind the common. prefix already used throughout main.go — so no new import is needed. If your editor reports a missing package, run go mod tidy inside gateway/.

Step 4: Build, deploy, and enable CORS​

  1. Verify the change compiles — run go build ./... inside gateway/ (or the repository root for the Go app). A compile error here usually points to a typo in the pasted cors.go.
  2. Build and deploy your updated app using its normal Extend build-and-deploy process.
  3. Set the CORS environment variables as described in Step 1: Set the CORS environment variables, then Restart and Apply.