HTTP Headers for API Testing: Content-Type, Accept, Caching and ETag
Headers are the small key-value lines that travel with every request and response. They decide whether the server reads your body as JSON, who you are, which format comes back, whether a response may be cached, and whether two people editing the same record overwrite each other. Many testers ignore headers and miss whole categories of bugs. In this lesson you will learn the headers that matter for API testing, with real-life examples and live calls.
Content-Type, Accept, Authorization, If-None-Match and If-Match. Key response headers are Content-Type, Location, Cache-Control, ETag, Allow, Retry-After and WWW-Authenticate. Testers verify that the right headers are sent, the right ones come back, caching behaves correctly (304 Not Modified) and security headers are present.What Are HTTP Headers? (The Parcel Label Example)
Headers are written as Name: value, one per line. Names are case-insensitive: content-type and Content-Type are the same header (Playwright returns all names in lower case).
Request Headers Every Tester Should Know
| Header | Example | Purpose | What to test |
|---|---|---|---|
Content-Type | application/json | Format of the body you send | Wrong type → 415 |
Accept | application/xml | Format you want back | Unsupported → 406 |
Authorization | Bearer eyJhbGci... | Who you are | Missing/expired/tampered → 401 |
X-API-Key | sqa-key-12345 | API key auth (custom header) | Missing → 401, wrong → 403 |
If-None-Match | "c2aee9e2..." | “Send only if changed” | Unchanged → 304 |
If-Match | "v3" | “Update only if still this version” | Stale → 412 |
User-Agent | MyApp/5.2 (Android 15) | Identifies the client | Version-specific behaviour |
Accept-Language | hi-IN | Preferred language | Localised messages |
X-Request-Id | 7f3c-91aa | Trace id for logs | Echoed back; appears in logs |
Idempotency-Key | ord-2026-0001 | Make POST retries safe | Same key → no duplicate |
Response Headers Every Tester Should Check
| Header | Example | Why it matters |
|---|---|---|
Content-Type | application/json; charset=UTF-8 | Client parses the body correctly; wrong type breaks apps |
Location | .../objects/obj_hoeovqif1v | URL of the newly created resource (201) |
ETag | "v1" | Version fingerprint for caching and locking |
Cache-Control | no-store, max-age=0 | Whether and how long to cache |
Allow | GET, HEAD | Methods allowed (with 405 and OPTIONS) |
Retry-After | 10 | When to retry after 429/503 |
WWW-Authenticate | Basic realm="SQA practice API" | Which auth scheme to use (with 401) |
Link | <...page=2>; rel="next" | Pagination navigation |
X-Total-Count | 12 | Total records for paging UIs |
X-RateLimit-Remaining | 4 | Calls left in the window |
See them live. Press Send and look at link and x-total-count in the output:
https://sqatools.in/wp-json/sqa-api/v1/users?per_page=2curl -i \
"https://sqatools.in/wp-json/sqa-api/v1/users?per_page=2"Content Negotiation: Content-Type vs Accept
These two are confused all the time:
Content-Typedescribes the body being sent (in a request: what you send; in a response: what the server sends).Acceptis a request header saying what format you would like back.
Content-Type is the language your order slip is written in. Accept is “please reply in English”. If the kitchen only understands Hindi slips, an English slip gets 415; if the waiter can’t reply in French, you get 406.https://sqatools.in/wp-json/sqa-api/v1/users/1curl -i \
-H "Accept: text/csv" \
"https://sqatools.in/wp-json/sqa-api/v1/users/1"The practice API answers 406 because it can return only JSON or XML. Change Accept to application/xml and you get XML.
Caching Headers: Cache-Control, ETag and 304
Caching saves bandwidth and makes apps fast. A product image or a catalogue page that hasn’t changed doesn’t need to be downloaded again.
Cache-Control
max-age=3600: the response can be reused for 1 hour.no-cache: may be stored, but must be revalidated with the server before use.no-store: never store it. Required for sensitive data such as bank balances, OTPs and personal details.privatevspublic: whether shared caches (CDN, proxies) may store it.
Cache-Control: public, max-age=600. A shared corporate proxy cached it, and the next employee saw a colleague’s balance. Testers should verify that every personal-data response has no-store or private.ETag and conditional requests
An ETag is a fingerprint of the current version of a resource. The client stores it and next time asks: “send it only if it changed” using If-None-Match.
GET /products/101→200,ETag: "c2aee9e25c4daa9445361e1bb4a749f7".GET /products/101withIf-None-Match: "c2aee9e25c4daa9445361e1bb4a749f7"→304 Not Modified, empty body.
https://sqatools.in/wp-json/sqa-api/v1/products/101curl -i \
-H "If-None-Match: \"c2aee9e25c4daa9445361e1bb4a749f7\"" \
"https://sqatools.in/wp-json/sqa-api/v1/products/101"If the browser shows 200 instead of 304 here, the product data has changed since this lesson was written; copy the new ETag from a normal GET and try again.
Optimistic Locking with If-Match (Prevent Lost Updates)
If-Match: "v3", B’s save is rejected with 412 Precondition Failed and B is told to reload.# 1. Create an object, note ETag "v1"
curl -i -X PUT "https://sqatools.in/wp-json/sqa-api/v1/objects/ticket-500" \
-H "Content-Type: application/json" -d '{"name":"Ticket 500","data":{"priority":"low"}}'
# 2. Update with the current version: 200, ETag becomes "v2"
curl -i -X PATCH ".../objects/ticket-500" -H "If-Match: \"v1\"" \
-H "Content-Type: application/json" -d '{"data":{"priority":"high"}}'
# 3. Update again with the OLD version: 412 Precondition Failed
curl -i -X PATCH ".../objects/ticket-500" -H "If-Match: \"v1\"" \
-H "Content-Type: application/json" -d '{"data":{"status":"closed"}}'
The practice API’s 412 message: If-Match “v1” does not match the current version “v2”. Someone changed the object; GET it again.
Security Headers to Verify
| Header | Expected value | Protects against |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | HTTPS downgrade attacks |
X-Content-Type-Options | nosniff | Browsers guessing the wrong content type |
Cache-Control | no-store for personal data | Data left in caches |
Access-Control-Allow-Origin | Specific trusted origins, not * with credentials | Cross-site data theft (CORS) |
No Server / X-Powered-By versions | Removed or generic | Attackers targeting known versions |
CORS is covered in detail in HTTPS, Cookies and CORS.
Testing Headers with Playwright
import { test, expect } from '@playwright/test';
const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';
test('JSON content type and no caching of user data', async ({ request }) => {
const res = await request.get(`${BASE}/users/1`);
const h = res.headers(); // all names are lower case
expect(h['content-type']).toContain('application/json');
expect(h['cache-control']).toContain('no-store');
expect(h['x-content-type-options']).toBe('nosniff');
});
test('ETag + If-None-Match gives 304', async ({ request }) => {
const first = await request.get(`${BASE}/products/101`);
const etag = first.headers()['etag'];
expect(etag).toBeTruthy();
const second = await request.get(`${BASE}/products/101`, {
headers: { 'If-None-Match': etag },
});
expect(second.status()).toBe(304);
expect((await second.body()).length).toBe(0);
});
test('stale If-Match is rejected with 412', async ({ request }) => {
const url = `${BASE}/objects/lock-${Date.now()}`;
await request.put(url, { data: { name: 'Ticket', data: { priority: 'low' }, ttl: 60 } }); // v1
await request.patch(url, { data: { data: { priority: 'high' } }, headers: { 'If-Match': '"v1"' } }); // v2
const stale = await request.patch(url, { data: { data: { status: 'closed' } }, headers: { 'If-Match': '"v1"' } });
expect(stale.status()).toBe(412);
});
test('pagination headers', async ({ request }) => {
const res = await request.get(`${BASE}/users?per_page=5`);
expect(res.headers()['x-total-count']).toBe('12');
expect(res.headers()['link']).toContain('rel="next"');
});
Header Testing Checklist
- Correct
Content-Typeon every response, including errors. - Missing/wrong
Content-Typeon requests gives 415, not 500. - Unsupported
Acceptgives 406. Locationon 201;Allowon 405;WWW-Authenticateon 401;Retry-Afteron 429/503.- Personal data is never publicly cacheable.
- ETag changes when the resource changes; 304 when it doesn’t; 412 on stale If-Match.
- No version-revealing headers; security headers present.
- Header names treated case-insensitively.
Interview Questions: HTTP Headers
What is the difference between Content-Type and Accept headers?
Content-Type describes the format of the body being sent (by the client in a request or by the server in a response). Accept is sent by the client to say which response formats it can handle. A wrong Content-Type leads to 415; an Accept the server can’t satisfy leads to 406.
What is an ETag?
An ETag is an identifier for a specific version of a resource returned in a response header. Clients send it back with If-None-Match to get 304 Not Modified when nothing changed, or with If-Match to update only if the resource is still at that version.
What is optimistic locking in APIs?
A concurrency control technique where updates include the expected version (If-Match with the ETag). If someone else changed the resource first, the server returns 412 Precondition Failed instead of silently overwriting their change.
What is the difference between no-cache and no-store?
no-cache allows storing the response but requires revalidation with the server before each reuse. no-store forbids storing it anywhere and is required for sensitive data.
Which headers do you check in API testing?
Content-Type, Cache-Control, Location for creates, ETag, Allow for 405, WWW-Authenticate for 401, Retry-After for 429 and 503, pagination headers, rate limit headers, CORS headers and security headers such as Strict-Transport-Security and X-Content-Type-Options.
Are HTTP header names case-sensitive?
No. Header names are case-insensitive, so tests should not depend on case. Header values may be case-sensitive depending on the header.
Where do you send an authentication token?
Usually in the Authorization header, for example Authorization: Bearer token. Some APIs use custom headers like X-API-Key. Tokens should not be sent in query strings because URLs are logged.
How does a client know how long to wait after 429 or 503?
From the Retry-After header, which contains seconds or a date. Rate limit headers such as X-RateLimit-Reset may also be provided.
What is the purpose of a request id header?
A request or correlation id (for example X-Request-Id) uniquely identifies a call across all services and logs. Including it in bug reports lets developers find the exact failing request.
How do you test that a response was cached correctly?
Check Cache-Control and ETag on the first response, send a conditional request with If-None-Match and expect 304, change the resource and expect 200 with a new ETag, and confirm that personal data responses are not cacheable.