π CORS β Cross-Origin Resource Sharing¶
By default, the browser blocks JavaScript on https://app.com from
fetch()-ing against https://api.anotherdomain.com. That's the Same-Origin
Policy, a basic defense against cross-site attacks.
CORS is the mechanism by which the server tells the browser
"yes, I accept requests from this origin, with these methods and these
headers". It does so with a handful of Access-Control-* headers.
Browser (app.com) Server (api.com)
β β
β OPTIONS /v1/users (preflight) β
β Origin: https://app.com β
β Access-Control-Request-Method: PUT β
βββββββββββββββββββββββββββββββββββββββ>β HIX_MwCors detects OPTIONS
β β Responds 204 with CORS headers
β 204 No Content β
β Access-Control-Allow-Origin: app.com β
β Access-Control-Allow-Methods: PUT,... β
β<βββββββββββββββββββββββββββββββββββββββ€
β β
β PUT /v1/users/42 (actual) β
β Origin: https://app.com β
βββββββββββββββββββββββββββββββββββββββ>β HIX_MwCors injects headers
β 200 OK + data β Rest of pipeline processes
β<βββββββββββββββββββββββββββββββββββββββ€
The preflight OPTIONS is launched by the browser automatically every time a cross-origin request uses a "non-simple" method (PUT, DELETE, PATCH) or custom headers (
Authorization,Content-Type: application/json).
When to use it¶
| Case | CORS |
|---|---|
| API consumed by SPA on another domain | β Yes β essential |
| Public API for integrations | β Yes |
| Mobile consuming the API | β No β no browser, doesn't apply |
| Backend same domain as frontend | β No β same-origin |
| Webhook receiving POSTs from external services | β No β servers don't respect CORS |
CORS protects the user, not the server. An attacker with
curlor their own server doesn't suffer CORS β the rule is enforced only by the browser.
Setup¶
HIX_MwCorsSetup( ;
"https://app.com", ; // cOrigin
"GET,POST,PUT,DELETE,OPTIONS,PATCH", ; // cMethods
"Content-Type,Authorization,X-Requested-With" ) // cHeaders
Default values if you don't call HIX_MwCorsSetup:
| Parameter | Default |
|---|---|
cOrigin |
"*" (any origin β permissive, development only) |
cMethods |
"GET,POST,PUT,DELETE,OPTIONS,PATCH" |
cHeaders |
"Content-Type,Authorization,X-Requested-With" |
Call before oSrv:Start().
Activation¶
HIX_MwCors is a global middleware β normally you apply it to the entire
server with oSrv:Use() so each response includes the headers:
Or on a specific route:
{ "name": "api.users", "url": "/api/users", "method": "GET",
"action": "controllers/api/users.prg",
"middleware": "HIX_MwCors" }
How it works¶
FUNCTION HIX_MwCors( oCtx )
LOCAL hCors := { ;
"Access-Control-Allow-Origin" => s_cCorsOrigin, ;
"Access-Control-Allow-Methods" => s_cCorsMethods, ;
"Access-Control-Allow-Headers" => s_cCorsHeaders, ;
"Access-Control-Max-Age" => "86400" ;
}
IF oCtx:oReq:cMethod == "OPTIONS"
oCtx:oReq:Respond( "", 204, "text", hCors ) // preflight
oCtx:lHandled := .T.
RETURN .F. // cuts the chain
ENDIF
hb_HMerge( oCtx:oReq:hExtraHeaders, hCors ) // injects in response
RETURN .T.
| Method | Behavior |
|---|---|
OPTIONS |
Responds 204 No Content with CORS headers β preflight resolved |
| Any other | Injects the Access-Control-* headers in the final response |
Access-Control-Max-Age: 86400 tells the browser to cache the
preflight response for 24 hours, avoiding an extra OPTIONS per request.
Combining with other middlewares¶
CORS usually goes first in the stack, before auth, so the preflight resolves without hitting a 401:
oSrv:Use( { "HIX_MwCors", "HIX_MwSession" } )
oSrv:AddRouteGet( "api.me", "/api/me", bAction, ;
"HIX_MwCors,HIX_MwJwt" )
If
HIX_MwJwtran before CORS, the OPTIONS without Bearer would receive a 401 and the browser would never make the real request.
Useful patterns¶
CORS open only in development¶
IF HIX_Config( "env" ) == "dev"
HIX_MwCorsSetup( "*" )
ELSE
HIX_MwCorsSetup( "https://app.com" )
ENDIF
Multiple origins (not natively supported)¶
HIX_MwCorsSetup accepts only one cOrigin. For multiple, write your own
middleware that looks at the Origin header and returns the appropriate header:
FUNCTION MyAppCors( oCtx )
LOCAL aAllowed := { "https://app.com", "https://admin.app.com" }
LOCAL cOrigin := oCtx:oReq:Header( "origin", "" )
IF AScan( aAllowed, cOrigin ) > 0
oCtx:oReq:hExtraHeaders[ "Access-Control-Allow-Origin" ] := cOrigin
ENDIF
RETURN HIX_MwCors( oCtx ) // delegates the rest to the standard middleware
Cross-origin cookies¶
If the API sends cookies (session) and the frontend is on another domain,
add Access-Control-Allow-Credentials: true and specify a concrete origin
(* is not compatible with credentials):
Common errors¶
| Symptom | Cause |
|---|---|
CORS policy: No 'Access-Control-Allow-Origin' |
Missing HIX_MwCors on the route β or not applied to OPTIONS |
Origin not allowed |
s_cCorsOrigin doesn't match the client's Origin |
Method PUT is not allowed |
s_cCorsMethods doesn't include PUT |
Header authorization is not allowed |
s_cCorsHeaders doesn't include Authorization |
| Preflight returns 401 | CORS configured after auth middleware |
Best practices¶
"*"only in development. In production, list the concrete origins that can consume your API.- CORS is not authentication. It only tells the browser which requests can complete β it authenticates nothing. Always combine it with JWT or Sessions.
- Apply with
oSrv:Use. The preflight OPTIONS reaches any URL, even nonexistent ones β registering it globally avoids surprises with 405/404 on OPTIONS. - Put CORS before auth in the pipeline. OPTIONS doesn't carry credentials and an auth middleware would reject it.
- Limit methods and headers. Don't expose everything if you don't use it.