GuidesWEB SERVERS
Fix CORS errors in Apache
The browser blocks client scripts from reading HTTP responses that lack matching Access-Control headers, so the web server must emit them. Configuring Apache CORS support means enabling mod_headers and attaching the right headers to responses and preflights. This guide applies when you control the Apache instance serving the API; if the API belongs to a third party, configuration changes on your side cannot fix it.
Confirm Apache is the right layer
A console CORS error means the browser received an HTTP response but refused to expose it to client code due to missing access headers. The fix belongs on the server answering the API request. When Apache serves that API, configure the headers inside a virtual host file, a <Directory> block, or an .htaccess file. Confirm whether the underlying issue is genuinely a missing CORS policy with the diagnosis guide.
If the API belongs to an external service, Apache configuration cannot resolve the error because you do not control the target server. For public GET and HEAD endpoints on any public HTTPS host, you can prefix the destination URL with https://proxy.cors.dev/. The proxy operator can observe transit traffic, so only query public data without credentials.
Enable mod_headers and set the headers
The Header directive requires mod_headers, an extension module enabled on Debian and Ubuntu using sudo a2enmod headers followed by a service restart. You can place the directive in server config, inside a <VirtualHost>, within a <Directory> block, or in .htaccess if AllowOverride FileInfo or AllowOverride All is set on the directory.
# enable first: sudo a2enmod headers && sudo systemctl restart apache2
# valid in a virtual host, a <Directory> block, or .htaccess
# (.htaccess requires AllowOverride FileInfo or All)
Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"The always condition forces Apache to attach the headers to both successful responses and error responses. The default condition, onsuccess, skips error responses, meaning a 404 or 500 response omits the headers and causes the browser to display a generic CORS error rather than the actual HTTP failure status. The always setting also persists headers across internal redirects, such as custom ErrorDocument handlers.
Whitelist your real origins
Hardcoding a single origin breaks access for additional frontends, while a wildcard * fails whenever requests send user credentials. The verified pattern matches the incoming Origin header against a regex whitelist with SetEnvIf and echoes the value back only on a match.
# echo the request Origin only when it matches the whitelist
SetEnvIf Origin ^https://(app\.example\.com|staging\.example\.com)$ CORS_ORIGIN=$0
# $0 is the whole matched origin; env= sends the header only on a match
Header always set Access-Control-Allow-Origin "%{CORS_ORIGIN}e" env=CORS_ORIGIN
Header always merge Vary "Origin"- The
SetEnvIfdirective sets theCORS_ORIGINenvironment variable only when the request origin matches the regular expression, and theenv=CORS_ORIGINcondition sends the header only then, so unlisted origins receive nothing. - The
$0backreference captures the entire matched origin string from the request. - The
Header always merge Vary "Origin"directive appendsOriginto theVaryheader, so intermediary caches do not serve a cached allowance to a different origin.
Answer the OPTIONS preflight
Requests sending JSON content types or custom headers trigger an OPTIONS preflight probe before the browser dispatches the actual request. If the backend application behind Apache does not handle OPTIONS requests, Apache can terminate them early using mod_rewrite, enabled via sudo a2enmod rewrite.
# enable first: sudo a2enmod rewrite
RewriteEngine On
RewriteCond %{REQUEST_METHOD} =OPTIONS
RewriteRule ^ - [R=204]
# 'always' attaches these to the 204 as well
Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
Header always set Access-Control-Max-Age "86400"Using R=204 is not an external redirect: passing a non-3xx status code to the R flag stops rewrite processing immediately and returns an empty 204 response. Any 2xx status satisfies the browser preflight check, and 204 No Content is the standard convention. Because the CORS headers are configured with always, they attach to this 204 response as well. If your backend framework already answers OPTIONS probes, skip the rewrite rule and let it handle preflights; the preflight guide explains what a passing response must contain.
Cookies, authorization, and the wildcard trap
Browsers reject cross-origin responses if the request includes credentials while Access-Control-Allow-Origin is set to *. Credentialed transfers require explicit origin verification.
- The client initiates the request with credentials enabled using
fetch(url, { credentials: 'include' }). - The server echoes the explicit matching origin using the
SetEnvIfpattern and setsHeader always set Access-Control-Allow-Credentials "true". - Never reflect arbitrary, untrusted request origins when credentials are enabled, as this creates an open session relay vulnerability.
Verify with curl
Using curl displays raw HTTP headers that the browser console obscures during cross-origin failures.
# 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 204 with allow-methods and allow-headers
curl -i -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type" \
https://api.example.com/statusExpect Access-Control-Allow-Origin on the standard GET request, and expect a 204 status with Access-Control-Allow-Methods and Access-Control-Allow-Headers on the OPTIONS preflight. Test a nonexistent path to confirm that CORS headers still appear on a 404 response, verifying that the always condition is functioning.
Good questions.
Why does Apache drop CORS headers on 404 and 500 error pages?
Apache applies the onsuccess condition by default, which skips error responses. Setting Header always attaches the headers to 4xx and 5xx responses, so the browser displays the true HTTP status code.
Can I use a wildcard for Access-Control-Allow-Origin with cookies?
No. Browsers reject wildcard origins when requests carry credentials. You must match the origin explicitly with SetEnvIf and return Access-Control-Allow-Credentials: true, as outlined in what CORS checks.
Should I handle OPTIONS requests in Apache or in my backend application?
If your application framework routes and answers OPTIONS requests correctly, let the application handle them. If your application returns errors on OPTIONS, short-circuit the preflight directly in Apache; the preflight guide details the required response headers.
How can I verify that my Apache CORS configuration is active?
Inspect raw responses using curl with an Origin header, or run your endpoint through the header checker to inspect the headers returned on both GET and OPTIONS requests.