GuidesWEB SERVERS
Fix CORS errors in IIS
The browser blocks client-side script access to API responses that lack matching Access-Control headers, so the web server must emit them. Resolving IIS CORS failures means configuring IIS to declare trusted origins and answer preflight requests. This guide applies when you operate the IIS instance hosting the API; if the API belongs to a third party, changes to your own server cannot fix it.
Confirm IIS is the right layer
A console CORS error indicates that the browser withheld an HTTP response because the response headers did not permit the calling origin. If IIS hosts the destination endpoint, you have two configuration paths: the official Microsoft IIS CORS module, which is the recommended approach covered here, or manual custom headers in web.config, which is strictly limited to simple requests. Verify that the failure originates from missing response headers by following the diagnosis guide.
When the destination is a third-party API outside your control, changes to a local IIS server cannot help. For public GET and HEAD requests to any public HTTPS host, you can prepend https://proxy.cors.dev/ to the target URL. The proxy operator can observe proxied traffic, so use it only for public data and never for requests carrying secrets or private tokens.
Install the CORS module and enable it
The IIS CORS module is a free Microsoft extension for IIS, distributed from the official IIS site. Once installed, it is configured per site or application inside the system.webServer block of web.config under its own cors configuration section.
<!-- web.config of the site or application; requires the IIS CORS module -->
<configuration>
<system.webServer>
<cors enabled="true" failUnlistedOrigins="true">
<add origin="https://app.example.com" maxAge="3600">
<allowHeaders allowAllRequestedHeaders="true" />
<allowMethods>
<add method="GET" />
<add method="POST" />
</allowMethods>
</add>
</cors>
</system.webServer>
</configuration>In this configuration, enabled="true" activates the module for the application. The origin rule explicitly whitelists the trusted origin, while allowAllRequestedHeaders="true" echoes whatever headers the client requests during preflight negotiation. The allowMethods element enumerates the HTTP verbs the preflight can approve, and maxAge="3600" lets the browser cache the preflight verdict for up to an hour (browsers apply their own caps).
Whitelist and reject origins
Each allowed origin requires its own add element under cors. Subdomain wildcards such as https://*.example.com are supported, the module always applies the most specific matching rule, and setting allowed="false" explicitly blocks an origin pattern.
- Setting
failUnlistedOrigins="true"causes IIS to reject requests from unlisted origins with an HTTP 403 status code instead of returning the response without CORS headers. - When configured with only a bare
*origin rule, the module returnsAccess-Control-Allow-Origin: *literally and omits theVary: Originheader. - When configured with specific origins, the module echoes the incoming request origin and appends
Vary: Origin, so downstream caches do not serve origin-specific responses to other callers.
Preflight handling, and why manual headers fall short
The IIS CORS module processes OPTIONS preflight requests before other IIS modules see them. Because browser preflight requests never transmit credentials, they arrive anonymously, and the module answers them correctly even when anonymous authentication is disabled on the site. This prevents the common preflight failures that occur under Windows authentication.
Adding headers manually through customHeaders attaches the origin header to responses, but it cannot answer preflight requests. IIS forwards OPTIONS requests to the underlying application or static file handler, which typically responds without Access-Control-Allow-Methods and Access-Control-Allow-Headers. As a result, non-simple requests such as JSON payloads, PUT, DELETE, and requests with custom headers fail in the browser.
<!-- attaches the header, but OPTIONS preflights still fail -->
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="https://app.example.com" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>The manual customHeaders route is viable only for simple requests that bypass preflight checks entirely. To inspect what a browser requires during preflight negotiation, review the preflight guide.
Cookies, authorization, and the wildcard trap
When a browser request includes credentials such as cookies or HTTP authentication, the browser discards the response if the server returns Access-Control-Allow-Origin: *. Credentialed requests require an explicit origin declaration and an explicit credentials permission header.
- The client initiates a credentialed transfer by setting
fetch(url, { credentials: 'include' }). - The IIS CORS module returns
Access-Control-Allow-Credentials: truewhenallowCredentials="true"is configured on the matching origin rule. - Microsoft's documentation states that
allowCredentialsshould be used only with a specific origin host name, never the wildcard*rule. Do not whitelist arbitrary origins when credential forwarding is active.
Verify with curl
Testing with curl displays the exact response headers returned by IIS without browser security policies obscuring the output.
# Simple GET: expect access-control-allow-origin in the response
curl -i -H "Origin: https://app.example.com" https://api.example.com/status
# Preflight: expect a 2xx status with access-control-allow-methods
curl -i -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
https://api.example.com/statusVerify that the GET response contains Access-Control-Allow-Origin matching your request. Next, confirm that the OPTIONS preflight probe returns an HTTP 2xx status code alongside Access-Control-Allow-Methods. If you enabled failUnlistedOrigins="true", test with an untrusted Origin header to verify that IIS returns a 403 status code.
Good questions.
Why does my OPTIONS request return 401 when Windows authentication is enabled?
Browsers send preflight requests without authentication headers, so they arrive anonymously. The IIS CORS module answers preflights before other IIS modules see them, including authentication. With manual custom headers the OPTIONS request still passes through authentication and the application pipeline, so a site that requires Windows authentication challenges the anonymous preflight with a 401.
Can I use wildcard origins together with credentials in IIS?
No. The CORS protocol requires an explicit origin matching the caller when credentials are included, and the IIS CORS module requires allowCredentials="true" to be set on a specific host rule rather than a wildcard.
Why do GET requests work in Postman but fail in the browser?
Postman does not enforce the browser same-origin policy or block responses with missing CORS headers. You can see how these execution environments differ in the Postman comparison.
What happens if an origin matches multiple rules in web.config?
The IIS CORS module applies the most specific rule. An exact match for a fully qualified host name takes precedence over a wildcard pattern such as a subdomain rule.
How can I check whether my IIS endpoints return valid preflight headers?
Send an OPTIONS request using curl or test the endpoint using the header checker to verify that Access-Control-Allow-Methods and the origin headers match your expectations.