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:

  1. Built-in platform CORS in Azure Functions
  2. Handling CORS in your function code
  3. 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.com
  • https://admin.example.com
  • http://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
https://app.example.com https://admin.example.com GET POST OPTIONS
authorization
content-type
etag
x-request-id
```text

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:

A ` B # A # I G c ` u # # f i c ` t # r # t e t e y H s e i 2 q 3 o u s x f . u . u b - t e r C y F s M s o o o t i f n u r s r r t r g w s o e r e i i n a o f t t n t l l r t h g e - - o i n w A n n ` e d o l t g A x r l e u p n l o n p t o e d w d r h s e - e o e d h O s f r d s e r e l i a i n i z h ` d g d g a e E e i s h t a T r n t i d a : c o e g s o n r ` e o ` s , t k i o r i e r a s s t ` e a o C - r o l g n i o a t m o u e i d t n t h t e e - h x n T e a t y a m i p d p c e e l a : r e t s e a , o d p f p o b l r d r i o o c r i w a e n s t q g e i u r o e t n s h c / t i r j s e s I d o D t e n s h n ` , o t r i o e o a f x u l t p g s e o h , n s l e y d t : o r t n i h g e t g m e . u r s s e p w r i e l f d l c i a g r h d t . o r I i f g i ` n O s P . T I O N S ` i s n t h a n d l e d c o r r e c t l y , t h e r e a l r e q u e s t n e v e r h a p p e n s .

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.