π¨ HTTP Headers¶
Every HTTP response carries a series of headers that the client reads before
the body: content type, cache, redirects, cookies, security,
... HIX manages them in oReq:hExtraHeaders and the U* helpers allow you
to read and write them without touching the object directly.
Browser ββββ GET /api/users βββββΆ HIX
β
β USetHeader( "Cache-Control", "no-store" )
β USetHeader( "X-Request-Id", "abc123" )
β USendJson( hData )
βΌ
<ββββ 200 OK βββββββββββββββ€
Content-Type: application/json
Cache-Control: no-store
X-Request-Id: abc123
...
Read headers from the request¶
| Call | Returns |
|---|---|
UHeader( "user-agent" ) |
User agent text or "" |
UHeader( "x-api-key", "" ) |
Header value or "" |
UHeader( "authorization" ) |
"Bearer eyJ..." |
UHeader( "accept" ) |
"application/json, text/html;q=0.9" |
FUNCTION _ApiHandler()
LOCAL cKey := UHeader( "x-api-key", "" )
IF Empty( cKey )
RETURN USendError( 401, "Missing API key" )
ENDIF
USendJson( { "ok" => .T. } )
RETURN NIL
Header names are case-insensitive -
UHeader("X-Api-Key")andUHeader("x-api-key")return the same value.
Write headers in the response¶
Accumulates the header in oReq:hExtraHeaders; when sending the response, HIX
adds them to the output:
USetHeader( "Cache-Control", "no-store" )
USetHeader( "X-Request-Id", "abc123" )
USetHeader( "X-Frame-Options", "DENY" )
USendJson( hData )
If you call the same header twice, the last one wins:
Direct access to hExtraHeaders¶
From a middleware with oCtx:oReq:
Equivalent to the helper, but only when you're not in a route action.
Common headers¶
Content type and encoding¶
| Header | Typical value |
|---|---|
Content-Type |
application/json; charset=utf-8 |
Content-Type |
text/html; charset=utf-8 |
Content-Type |
text/plain; charset=utf-8 |
Content-Encoding |
gzip (managed by the dispatcher) |
HIX sets Content-Type automatically based on the helper you use
(USendJson β json, USendHtml β html, ...). Only override it if
you want something non-standard:
Cache¶
| Header | For what |
|---|---|
Cache-Control: no-store |
Never cache (login, sensitive data) |
Cache-Control: no-cache |
Cache, but always revalidate |
Cache-Control: max-age=3600 |
Cache for 1 hour |
Cache-Control: public, max-age=31536000, immutable |
Versioned assets |
ETag |
HIX calculates it automatically for static files |
USetHeader( "Cache-Control", "no-store, no-cache, must-revalidate" )
USetHeader( "Pragma", "no-cache" )
USendJson( hUserPrivateData )
Redirects and location¶
| Header | When |
|---|---|
Location: /new-url |
Accompanies 301/302/307 |
Refresh: 5; url=/home |
Browser redirect after N seconds |
Better to use URedirect("/new", 302) than to set Location manually.
Security¶
| Header | Recommended value |
|---|---|
X-Frame-Options |
DENY or SAMEORIGIN - anti-clickjacking |
X-Content-Type-Options |
nosniff - disables MIME sniffing |
Strict-Transport-Security |
max-age=31536000; includeSubDomains (HTTPS only) |
Referrer-Policy |
strict-origin-when-cross-origin |
Content-Security-Policy |
default-src 'self' (CSP - requires per-app tuning) |
Permissions-Policy |
geolocation=(), camera=() (disables APIs) |
PROCEDURE _ApplySecurityHeaders()
USetHeader( "X-Frame-Options", "DENY" )
USetHeader( "X-Content-Type-Options", "nosniff" )
USetHeader( "Referrer-Policy", "strict-origin-when-cross-origin" )
IF UIsHttps()
USetHeader( "Strict-Transport-Security", "max-age=31536000; includeSubDomains" )
ENDIF
RETURN
Better to convert it into a global middleware:
FUNCTION MyAppSecurityHeaders( oCtx )
_ApplySecurityHeaders()
RETURN .T.
oSrv:Use( "MyAppSecurityHeaders" )
CORS¶
The Access-Control-* headers are managed by the
HIX_MwCors middleware. Don't set them manually:
Cookies¶
Set-Cookie is special: it can appear multiple times in the same
response (one per cookie). Use USetCookie instead of USetHeader:
USetCookie( "session", cSid, 3600 )
USetCookie( "lang", "es", 86400 * 30 )
// β two Set-Cookie in the response
See Cookies.
Application custom¶
| Convention | Use |
|---|---|
X-Request-Id: abc123 |
Request identifier - useful for logs |
X-RateLimit-Remaining: 42 |
How many requests remain in the window |
X-RateLimit-Reset: 1735689600 |
When the window resets |
X-Powered-By: HIX |
Optional seal (better not to expose) |
LOCAL nCount := UContext():hData[ "rate_count" ]
USetHeader( "X-RateLimit-Limit", "100" )
USetHeader( "X-RateLimit-Remaining", hb_NToS( 100 - nCount ) )
Useful patterns¶
Force file download¶
USetHeader( "Content-Type", "application/octet-stream" )
USetHeader( "Content-Disposition", 'attachment; filename="export.csv"' )
USendText( cCsv )
SSE stream - disable buffering¶
USendStreamStart( "text/event-stream", 200, { ;
"Cache-Control" => "no-cache", ;
"X-Accel-Buffering" => "no" ; // nginx won't buffer
} )
Pass request-id between frontend and backend¶
FUNCTION MyAppRequestId( oCtx )
LOCAL cId := UHeader( "X-Request-Id", "" )
IF Empty( cId )
cId := hb_MD5( hb_NToS( Int( hb_TToSec( hb_DateTime() ) ) ) + ;
hb_NToS( hb_Random() ) )
cId := Left( cId, 16 )
ENDIF
oCtx:hData[ "request_id" ] := cId
USetHeader( "X-Request-Id", cId )
RETURN .T.
Best practices¶
- Don't expose sensitive information. No
Server: HIX 1.2.3orX-Powered-Byor stack traces in headers. The less an attacker knows, the better. - Security by default.
X-Frame-Options,nosniff,Referrer-Policyand CSP should go in a global middleware across your entire app. - Don't put data in headers. Headers are for metadata; the body is for data. Headers > 8KB can be rejected by proxies.
- Case-insensitive when reading, canonical when writing. HTTP defines
headers as case-insensitive, but proxies prefer the canonical form:
Content-Type, notcontent-type. - Don't duplicate
Set-Cookiemanually. Always useUSetCookie- it knows how to manage the cookie list without overwriting them.