HTTP Headers: Content Negotiation, Auth, Caching, and CORS
Understand the purpose of Content-Type, Accept, Authorization, Cookie, Set-Cookie, Cache-Control, ETag, Location, Origin, CORS headers, and common security headers, and know how to read and set each in PHP.
Headers are how HTTP requests and responses carry metadata alongside the method/status/body — they describe the body's format, identify and authenticate the client, control caching and browser security behavior, and drive cross-origin access rules. This lesson goes through every header named in the spec: Content-Type, Accept, Authorization, Cookie, Set-Cookie, Cache-Control, ETag, Location, Origin, the CORS family, and common security headers, each with a PHP header() example.
Describing the body: Content-Type and Accept
Content-Type describes the format of a message's body — on a request, it tells the server how to parse the body being sent (application/json, application/x-www-form-urlencoded, multipart/form-data for file uploads); on a response, it tells the client how to parse what's coming back (application/json, text/html, image/png). Getting Content-Type wrong on a response is one of the most common causes of a browser rendering JSON as a downloadable file, or an API client failing to parse a response it received correctly.
Accept is the request-side mirror of Content-Type: it's the client telling the server which content types it's willing to receive, in preference order (e.g. Accept: application/json, text/html;q=0.9). A well-behaved API can inspect Accept and choose to return JSON vs. XML vs. HTML accordingly — this is called content negotiation, though many simple APIs just always return JSON regardless.
Identity and access: Authorization, Cookie, Set-Cookie, Origin
Authorization carries credentials on a request — commonly Authorization: Bearer <token> for API tokens/JWTs, or Authorization: Basic <base64> for HTTP Basic auth. It's the standard place to put API authentication, as opposed to cookies which are more common for browser-based session login.
Set-Cookie (response) and Cookie (request) work as a pair: the server issues one or more Set-Cookie headers to store small values in the browser, and the browser automatically echoes them back via a single Cookie header on every subsequent request to a matching domain/path. Set-Cookie also carries attributes controlling behavior: HttpOnly (JavaScript can't read the cookie, mitigating XSS theft), Secure (only sent over HTTPS), SameSite (controls whether the cookie is sent on cross-site requests, mitigating CSRF), Path, Domain, and Max-Age/Expires.
Origin is a request header the browser sets automatically on cross-origin requests (and some same-origin ones), identifying the scheme+host+port the request came from (e.g. https://app.example.com) — it's the header CORS logic on the server inspects to decide whether to allow the request.
Caching: Cache-Control, ETag, and Location
Cache-Control is the primary header controlling caching behavior for both browsers and intermediate proxies/CDNs — directives include no-store (never cache), no-cache (cache it, but always revalidate with the server first), public/private (whether shared caches like CDNs may store it), and max-age=N (how many seconds a cached copy is considered fresh without revalidation). ETag is an opaque identifier (often a hash) representing a specific version of a resource; a client can send it back in If-None-Match on a later request, and the server replies 304 Not Modified if nothing changed, avoiding re-sending an unchanged body (as shown in the status codes lesson).
Location is used two different ways depending on the status code: on a 3xx redirect response, it tells the client where to go instead; on a 201 Created response, it tells the client the URL of the resource that was just created — both uses point 'here is the relevant URL for this outcome'.
CORS and security headers
CORS (Cross-Origin Resource Sharing) is the browser mechanism that decides whether JavaScript running on one origin (e.g. app.example.com) may read a response from a different origin (e.g. api.example.com). The server opts in with response headers: Access-Control-Allow-Origin (which origin(s) may read the response), Access-Control-Allow-Methods and Access-Control-Allow-Headers (what the preflight OPTIONS request is permitted to use), and Access-Control-Allow-Credentials (whether cookies/Authorization may be included). Without the right CORS headers, the browser blocks the JavaScript from reading the response even though the HTTP request itself succeeded — this is a browser-enforced restriction, not a server-side one, which is why it never shows up when testing with curl or Postman.
Common security-hardening response headers include Strict-Transport-Security (tells the browser to only ever use HTTPS for this domain from now on), X-Content-Type-Options: nosniff (stops the browser from guessing/overriding the declared Content-Type, closing an old attack vector), X-Frame-Options / Content-Security-Policy (control whether the page can be embedded in an iframe, mitigating clickjacking), and Content-Security-Policy more broadly (restricts which sources scripts/styles/images may load from, mitigating XSS impact). None of these are required by HTTP itself, but they're standard practice on any production web application.
• Access-Control-Allow-Origin — which origin(s) may read this cross-origin response • Access-Control-Allow-Methods / -Headers — what a CORS preflight (OPTIONS) permits • Access-Control-Allow-Credentials — whether cookies/Authorization travel with the cross-origin request • Strict-Transport-Security — force HTTPS-only for this domain going forward • X-Content-Type-Options: nosniff — stop MIME-type sniffing • Content-Security-Policy — restrict allowed script/style/resource sources
Setting headers in PHP
Reading a request header uses $_SERVER['HTTP_<NAME_WITH_UNDERSCORES>'], or the more convenient getallheaders() function which returns an associative array with original casing. setcookie() is PHP's dedicated function for building a correct Set-Cookie header — prefer it over hand-writing header('Set-Cookie: ...') so attribute formatting and encoding are handled correctly.
<?php
// Content negotiation
header('Content-Type: application/json; charset=utf-8');
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
// Authentication
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
// Cookies (Set-Cookie response header, built via setcookie())
setcookie('session_id', 'abc123', [
'expires' => time() + 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
// Caching
header('Cache-Control: public, max-age=300');
header('ETag: "resource-v7"');
// Redirect / created-location
header('Location: /orders/501');
// CORS
header('Access-Control-Allow-Origin: https://app.example.com');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// Security headers
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header('X-Content-Type-Options: nosniff');
header("Content-Security-Policy: default-src 'self'");Code Examples
getallheaders() gives a clean, correctly-cased view of every request header, as an alternative to picking through $_SERVER['HTTP_*'] entries by hand. Expected result: application/json Bearer eyJhbGciOi...
HttpOnly stops JavaScript from reading the cookie (blunting XSS-based theft), Secure ensures it's only sent over HTTPS, and SameSite=Strict blocks it from being sent on cross-site requests at all (strong CSRF protection). The security headers alongside it harden the response itself against MIME-sniffing and restrict script sources.
'httponly' => true, 'samesite' => 'Strict', This echoes back Origin only when it's on an explicit allowlist (never a bare wildcard when credentials are involved — browsers reject Access-Control-Allow-Origin: * combined with Allow-Credentials: true), adds Vary: Origin so shared caches don't serve one origin's CORS headers to another, answers the OPTIONS preflight with the allowed methods/headers, and layers in ETag-based conditional caching on the real GET. Access-Control-Allow-Credentials: true is required whenever the cross-origin request needs to send cookies or an Authorization header; without it, the browser strips credentials from the request or blocks reading the response.
<?php
// Reading Authorization and Content-Type on an incoming request
$headers = getallheaders();
echo $headers['Content-Type'] ?? 'no content-type', "\n";
echo $headers['Authorization'] ?? 'no auth header', "\n";<?php
// Issuing a session cookie and a security-hardened response together
setcookie('session_id', bin2hex(random_bytes(16)), [
'expires' => time() + 7200,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict',
]);
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
header("Content-Security-Policy: default-src 'self'");
http_response_code(200);
echo json_encode(['status' => 'logged in']);<?php
// A CORS-aware API endpoint handling preflight and the real request
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins, true)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Credentials: true');
header('Vary: Origin');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
http_response_code(204);
exit;
}
// Conditional caching for the real GET
$currentEtag = '"v9"';
if (($_SERVER['HTTP_IF_NONE_MATCH'] ?? null) === $currentEtag) {
http_response_code(304);
exit;
}
header("ETag: $currentEtag");
header('Cache-Control: private, max-age=30');
header('Content-Type: application/json');
http_response_code(200);
echo json_encode(['id' => 9, 'name' => 'Keyboard']);Important Rules
Always set an accurate Content-Type on every response body you produce.
Mark cookies HttpOnly and Secure by default; add SameSite to control cross-site sending, unless you have a specific reason not to.
Never pair Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — browsers reject that combination, and for good reason.
Use ETag + If-None-Match (304) to avoid re-sending unchanged bodies rather than relying on max-age alone for frequently-revalidated resources.
Add baseline security headers (nosniff, HSTS, a Content-Security-Policy) to production responses even when not explicitly asked for — they cost little and close common attack vectors.
Where this shows up
Debugging 'my fetch() call works in curl but fails in the browser with a CORS error' is one of the most common real-world PHP/web-developer troubleshooting sessions, and it's always resolved by inspecting the exact response headers, not the body. Security scanners and tools like Mozilla Observatory grade production sites specifically on the presence of headers like Strict-Transport-Security, X-Content-Type-Options, and Content-Security-Policy.
<?php header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Credentials: true');
<?php $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (in_array($origin, ['https://app.example.com'], true)) { header("Access-Control-Allow-Origin: $origin"); header('Access-Control-Allow-Credentials: true'); }
A wildcard origin combined with credentials is explicitly disallowed by browsers (and would be a serious security hole if it worked) — any site could then make credentialed requests to your API. Echo back a specific, allowlisted origin instead.
What is the purpose of the HttpOnly attribute on a cookie? A. It makes the cookie only sent over HTTP, never HTTPS B. It prevents JavaScript from reading the cookie's value, mitigating XSS-based theft C. It makes the cookie expire after one HTTP request D. It disables the cookie entirely in modern browsers
B. It prevents JavaScript from reading the cookie's value, mitigating XSS-based theft — HttpOnly stops document.cookie (and any other JavaScript API) from reading that cookie, so even if an attacker manages to inject a script (XSS), they can't steal the session cookie's value directly.
Why does a CORS error appear when testing a request in the browser but not when using curl? A. curl is faster than the browser and avoids timeouts B. CORS is a browser-enforced restriction on which origins JavaScript may read cross-origin responses from — it does not stop the underlying HTTP request from happening C. curl automatically adds the correct CORS headers D. Servers only check CORS headers for requests from certain IP ranges
B. CORS is a browser-enforced restriction on which origins JavaScript may read cross-origin responses from — it does not stop the underlying HTTP request from happening — The HTTP request/response still completes normally in both cases; CORS is a rule enforced by the browser about whether the calling JavaScript is allowed to read the result, which is why curl (with no such restriction) shows no error.
Which header tells a client the format of a response body, such as application/json or text/html? A. Accept B. Content-Type C. Origin D. Cache-Control
B. Content-Type — Content-Type describes the format of the body that accompanies it — on a response, it tells the receiving client how to parse the bytes that follow. Accept is the request-side header expressing what formats the client would prefer to receive.
Write a PHP script that sets a session cookie named 'sid' (HttpOnly, Secure, SameSite=Lax, 1 hour expiry), sets Content-Type: application/json, sets Cache-Control: no-store, and echoes a JSON success body — appropriate for a login endpoint response where the response should never be cached.
<?php setcookie('sid', bin2hex(random_bytes(16)), [ 'expires' => time() + 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]); header('Content-Type: application/json'); header('Cache-Control: no-store'); http_response_code(200); echo json_encode(['status' => 'logged in']);
no-store is stronger than no-cache: it tells caches not to store the response at all, appropriate for anything containing sensitive/session data.