CORS on Azure Functions looks simple until you ship something with auth, multiple environments, and a frontend team that keeps changing origins every sprint.
I’ve seen teams treat CORS as a checkbox in the Azure Portal, then spend hours debugging why Authorization headers fail, why local dev works but production doesn’t, or why preflight requests get blocked before their function code even runs.
If you’re building browser-facing APIs on Azure Functions, you have a few ways to handle CORS. Some are easy. Some are flexible. Some are traps.
The short version
For Azure Functions, you’ll usually choose between:
- Built-in platform CORS in Azure Functions
- Handling CORS in your function code
- Handling CORS at a proxy or gateway layer
- Azure API Management
- Front Door / reverse proxy setups
- App Gateway in some architectures
My opinion: for most teams, use platform CORS for simple cases and move CORS to API Management or another gateway for serious production setups. Only handle CORS in function code if you really need per-route or dynamic behavior and you understand the risks.
Quick CORS refresher
Browsers enforce CORS, not servers. Your Azure Function can return a perfectly valid JSON response, and the browser can still block it if the right headers aren’t present.
The most common header is:
Access-Control-Allow-Origin: https://app.example.com
Sometimes APIs go fully public with:
Access-Control-Allow-Origin: *
A real example from api.github.com:
access-control-allow-origin: *
access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
That access-control-expose-headers list matters. Without it, frontend code can’t read most custom response headers even if they’re present.
Option 1: Built-in Azure Functions CORS
Azure Functions has built-in CORS configuration in the platform. You set allowed origins in the Azure Portal or through config, and Azure adds the right headers.
This is the default choice for a lot of teams because it’s dead simple.
Pros
- Fast to set up
- No app code changes
- Works well for basic browser clients
- Good fit for one frontend + one API setups
- Preflight handling is mostly automatic
Cons
- Limited flexibility
- Not great for dynamic origin logic
- Can get awkward across multiple environments
- Easy to misconfigure with credentials
- Per-route customization is weak
Best fit
Use built-in CORS when:
- You have a small number of known frontend origins
- Your API is straightforward
- You don’t need route-specific CORS behavior
- You want the lowest operational overhead
Example allowed origins
Typical setup:
https://app.example.comhttps://admin.example.comhttp://localhost:5173
That’s enough for a lot of SPAs.
Practical warning
If you rely on cookies or authenticated browser requests, CORS gets stricter. You can’t combine:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
That’s invalid for credentialed browser requests. If your app uses cookies, session auth, or credentialed fetch, you need explicit origins.
Official docs: Azure Functions CORS
Option 2: Handle CORS in Azure Function code
You can add CORS headers yourself in each function response and manually handle OPTIONS preflight requests.
This gives you maximum control, which sounds great right up until you’re maintaining it across 20 endpoints.
Pros
- Full control
- Can support dynamic origin allowlists
- Can vary by route, tenant, or environment
- Lets you expose custom response headers precisely
Cons
- Easy to get wrong
- You must handle preflight properly
- Inconsistent behavior across endpoints is common
- More code, more tests, more bugs
- Can conflict with platform-level CORS if both are enabled
Best fit
Use code-level CORS when:
- You need per-tenant origin validation
- You expose different headers on different endpoints
- You need custom CORS logic that the platform can’t express
- You are very deliberate about testing
JavaScript example
module.exports = async function (context, req) {
const allowedOrigins = new Set([
"https://app.example.com",
"http://localhost:5173"
]);
const origin = req.headers.origin;
const isAllowed = origin && allowedOrigins.has(origin);
if (req.method === "OPTIONS") {
context.res = {
status: 204,
headers: isAllowed ? {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "3600",
"Vary": "Origin"
} : {}
};
return;
}
context.res = {
status: 200,
headers: isAllowed ? {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Expose-Headers": "ETag, X-Request-Id",
"Vary": "Origin"
} : {},
body: { ok: true }
};
};
```text
A few things here matter:
- `Vary: Origin` helps caches behave correctly
- `OPTIONS` returns early
- `Access-Control-Expose-Headers` is included if the frontend needs to read custom headers
- The origin is reflected only if it’s allowed
### My take
If you go this route, disable or carefully align platform CORS. Two layers trying to “help” usually creates weird responses.
Official docs: [Azure Functions HTTP trigger](https://learn.microsoft.com/azure/azure-functions/functions-bindings-http-webhook-trigger)
## Option 3: Handle CORS in API Management or a gateway
This is the grown-up option for larger systems.
If your Azure Functions sit behind Azure API Management, put CORS there unless you have a strong reason not to. You get centralized policy management, consistent behavior across services, and fewer surprises when teams add new backends.
### Pros
- **Centralized CORS policy**
- **Consistent across multiple APIs**
- **Easier governance**
- **Better fit for enterprise environments**
- **Lets function code stay focused on business logic**
### Cons
- **More infrastructure**
- **More cost**
- **Can complicate debugging if you forget where headers are added**
- **Another layer to configure for local and staging environments**
### Best fit
Use gateway-level CORS when:
- Multiple frontends call multiple Azure Functions
- You already use API Management
- You need policy standardization
- You want security controls in one place
### API Management policy example
That’s cleaner than duplicating CORS logic in every function.
Official docs: CORS policy in Azure API Management
Comparison table
Built-in Azure Functions CORS
Pros
- Simple
- Low maintenance
- Good default for small apps
Cons
- Limited flexibility
- Harder for advanced auth and multi-tenant cases
Function code CORS
Pros
- Maximum control
- Dynamic logic possible
Cons
- Easy to break
- Lots of repetition
- Preflight handling becomes your problem
Gateway / API Management CORS
Pros
- Centralized
- Scales well across teams and services
- Cleaner separation of concerns
Cons
- Added complexity
- Added cost
- Requires discipline in layered architecture
Common Azure Functions CORS mistakes
1. Allowing * when you need credentials
This is the classic one. Public APIs can often use:
access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Most APIs don’t need that many, but the pattern is right.
### 4. No `Vary: Origin`
If you reflect the request origin dynamically and a CDN or proxy caches the response, missing `Vary: Origin` can cause the wrong origin to get served.
### 5. Config drift between environments
Localhost works. Staging breaks. Production half-works. This usually means your origin allowlist is being managed manually in too many places.
## What I recommend
### For small apps
Use **Azure Functions built-in CORS**.
### For multi-service or enterprise setups
Use **API Management or a gateway**.
### For advanced tenant-aware rules
Use **code-level CORS**, but only if you really need it and you test preflight paths explicitly.
## A sane production checklist
- Allow only known origins for authenticated browser apps
- Handle `OPTIONS` correctly somewhere in the stack
- Expose headers your frontend actually reads
- Add `Vary: Origin` when reflecting origins
- Don’t configure CORS in multiple layers unless the behavior is intentional
- Keep local dev origins explicit
- Test with real browser requests, not just Postman or curl
That last one matters. Postman does not enforce browser CORS rules, so it happily hides broken configs.
If you’re tightening browser-facing security beyond CORS, review your other headers too. CORS controls who can read responses in the browser, but it doesn’t replace things like CSP or frame protections. If you need a guide for that side of the stack, [CSP Guide](https://csp-guide.com) is worth keeping around.