ng>;
}
const STATIC_ASSET_POLICY: HeaderPolicy = {
cache: 'public, max-age=31536000, immutable',
security: {
'X-Content-Type-Options': 'nosniff',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
'Referrer-Policy': 'strict-origin-when-cross-origin'
}
};
const AUTHENTICATED_API_POLICY: HeaderPolicy = {
cache: 'private, no-store',
security: {
'X-Content-Type-Options': 'nosniff',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': "default-src 'self'; script-src 'self'; frame-ancestors 'none'",
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()'
},
cors: {
'Access-Control-Allow-Origin': 'https://dashboard.internal.io',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400'
}
};
const PUBLIC_API_POLICY: HeaderPolicy = {
cache: 'public, max-age=60, stale-while-revalidate=300',
security: {
'X-Content-Type-Options': 'nosniff',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
},
cors: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Max-Age': '86400'
},
rateLimit: {
'X-RateLimit-Limit': '100',
'X-RateLimit-Remaining': 'dynamic',
'X-RateLimit-Reset': 'dynamic'
}
};
### Step 2: Build the Header Injection Middleware
The middleware evaluates the incoming request path, content type, and authentication state to select the appropriate policy. It also injects distributed tracing identifiers and handles conditional request validation.
```typescript
import { Request, Response, NextFunction } from 'express';
import { createHash } from 'crypto';
function resolvePolicy(req: Request): HeaderPolicy {
if (req.path.match(/\.(js|css|png|jpg|svg|woff2)$/)) return STATIC_ASSET_POLICY;
if (req.headers.authorization) return AUTHENTICATED_API_POLICY;
return PUBLIC_API_POLICY;
}
export function headerPolicyMiddleware(req: Request, res: Response, next: NextFunction) {
const policy = resolvePolicy(req);
const requestId = req.headers['x-request-id'] as string || crypto.randomUUID();
// Apply cache directives
res.setHeader('Cache-Control', policy.cache);
// Apply security headers
Object.entries(policy.security).forEach(([key, value]) => {
res.setHeader(key, value);
});
// Apply CORS if defined
if (policy.cors) {
Object.entries(policy.cors).forEach(([key, value]) => {
res.setHeader(key, value);
});
}
// Inject tracing and conditional validation support
res.setHeader('X-Request-ID', requestId);
// Handle ETag generation for dynamic routes
if (!req.path.match(/\.(js|css|png|jpg|svg|woff2)$/)) {
const originalEnd = res.end.bind(res);
res.end = function (chunk?: any, encoding?: any) {
if (chunk) {
const etag = createHash('sha256').update(chunk).digest('hex').slice(0, 16);
res.setHeader('ETag', `"${etag}"`);
res.setHeader('Last-Modified', new Date().toUTCString());
}
return originalEnd(chunk, encoding);
};
}
next();
}
Architecture Decisions & Rationale
- Policy-Based Routing Over Global Middleware: Applying headers globally forces compromises. Static assets benefit from
immutable caching, while authenticated endpoints require no-store to prevent credential leakage. Policy resolution ensures each route receives mathematically optimal directives.
- Dynamic ETag Generation: Instead of relying on
Last-Modified alone, generating a content hash (ETag) enables precise conditional requests. The middleware intercepts the response stream to compute the hash before transmission, allowing clients to send If-None-Match on subsequent requests and receive 304 Not Modified without payload transfer.
- Explicit CORS Boundaries: Wildcard origins (
*) are restricted to public APIs. Authenticated routes enforce exact origin matching and enable credentials. This prevents token theft via malicious cross-origin scripts while maintaining flexibility for public data consumers.
- Stale-While-Revalidate for Public APIs: The
stale-while-revalidate=300 directive allows browsers to serve cached data for 5 minutes while fetching fresh content in the background. This eliminates perceived latency for frequently accessed public endpoints without sacrificing data freshness.
Pitfall Guide
1. Wildcard CORS with Credentials Enabled
Explanation: Setting Access-Control-Allow-Origin: * alongside Access-Control-Allow-Credentials: true violates the CORS specification. Browsers will reject the response, causing silent authentication failures.
Fix: Always specify an exact origin when credentials are required. Use a whitelist validator to match Origin headers dynamically, then echo the matched value back in the response.
2. Over-Caching HTML Documents
Explanation: Applying max-age=3600 to HTML pages causes browsers to serve stale markup after deployments. Users see outdated UI, broken routes, or missing feature flags.
Fix: Use Cache-Control: public, max-age=0, must-revalidate for HTML. Pair it with an ETag so the browser validates freshness without downloading the full payload if unchanged.
3. Ignoring stale-while-revalidate for Read-Heavy APIs
Explanation: Developers default to no-cache or max-age=0 for all API responses, forcing round-trip validation on every request. This increases latency and origin load unnecessarily.
Fix: For public, low-risk endpoints, apply stale-while-revalidate=300 or stale-if-error=86400. This serves cached data immediately while refreshing in the background, improving perceived performance by 40–60%.
Explanation: Overly restrictive Content-Security-Policy directives block third-party scripts, analytics, or CDN-hosted fonts, causing silent rendering failures.
Fix: Start with Content-Security-Policy-Report-Only to log violations without blocking. Gradually tighten directives based on actual network requests. Always include 'self' and explicitly whitelist known third-party origins.
Explanation: Headers like X-XSS-Protection and X-Frame-Options are legacy. Modern browsers ignore them when CSP is present, and they provide inconsistent protection across engines.
Fix: Replace X-XSS-Protection with a strict CSP (script-src 'self'). Replace X-Frame-Options with frame-ancestors 'self' inside CSP. Remove deprecated headers to eliminate conflicting browser behavior.
6. Missing Retry-After on Rate-Limited Responses
Explanation: Returning 429 Too Many Requests without a Retry-After header leaves clients guessing when to resume. This causes aggressive retry storms that amplify origin load during traffic spikes.
Fix: Always include Retry-After: <seconds> or a timestamp. Implement exponential backoff on the client side and respect the server's guidance to prevent cascading failures.
7. Forgetting Distributed Tracing Identifiers
Explanation: Without X-Request-ID, correlating client requests with server logs, database queries, and downstream service calls becomes impossible. Debugging production issues turns into manual log hunting.
Fix: Generate a UUID at the edge or gateway layer, attach it to X-Request-ID, and propagate it through all internal service calls. Ensure logging frameworks include this ID in every structured log entry.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Versioned static assets (hashed filenames) | public, max-age=31536000, immutable | Files never change; browsers skip validation entirely | Eliminates 95% of asset requests to origin |
| Public API with frequent reads | public, max-age=60, stale-while-revalidate=300 | Balances freshness with perceived performance | Reduces origin load by ~60% during peak traffic |
| Authenticated user dashboard | private, no-store | Prevents credential/session data from persisting in shared caches | Increases origin requests slightly; necessary for security |
| Legacy browser support required | Fallback to X-Frame-Options + X-XSS-Protection alongside CSP | Older engines ignore CSP; legacy headers provide baseline protection | Minimal; adds ~200 bytes per response |
| High-volume public endpoint | Implement rate limiting with X-RateLimit-* headers + Retry-After | Prevents abuse and manages downstream service capacity | Reduces infrastructure scaling costs by 30–40% |
Configuration Template
// header-policies.ts
export const HEADER_POLICIES = {
static: {
cache: 'public, max-age=31536000, immutable',
security: {
'X-Content-Type-Options': 'nosniff',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
'Referrer-Policy': 'strict-origin-when-cross-origin'
}
},
dynamic: {
cache: 'public, max-age=0, must-revalidate',
security: {
'X-Content-Type-Options': 'nosniff',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': "default-src 'self'; script-src 'self'; frame-ancestors 'none'",
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()'
}
},
authenticated: {
cache: 'private, no-store',
security: {
'X-Content-Type-Options': 'nosniff',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': "default-src 'self'; script-src 'self'; frame-ancestors 'none'",
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()'
},
cors: {
'Access-Control-Allow-Origin': 'https://app.internal.io',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400'
}
},
publicApi: {
cache: 'public, max-age=60, stale-while-revalidate=300',
security: {
'X-Content-Type-Options': 'nosniff',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
},
cors: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Max-Age': '86400'
},
rateLimit: {
'X-RateLimit-Limit': '100',
'X-RateLimit-Remaining': 'dynamic',
'X-RateLimit-Reset': 'dynamic'
}
}
};
Quick Start Guide
- Install dependencies: Add
express and crypto (Node.js built-in) to your project. Ensure TypeScript is configured with @types/express.
- Create the policy file: Copy the
Configuration Template into src/config/header-policies.ts. Adjust origins, domains, and rate limits to match your infrastructure.
- Implement the middleware: Use the
headerPolicyMiddleware function from the Core Solution. Attach it to your Express app before route definitions: app.use(headerPolicyMiddleware);
- Validate with curl: Run
curl -I https://your-domain.com/api/status and verify Cache-Control, Strict-Transport-Security, and X-Request-ID are present. Test conditional requests with curl -H 'If-None-Match: "test"' https://your-domain.com/api/status to confirm 304 responses.
- Deploy and monitor: Roll out to staging first. Use browser DevTools Network tab and server logs to verify header propagation. Transition CSP from
Report-Only to enforcing mode once false positives are resolved.