An AI-written HTTP server can return 304 Not Modified and still violate the contract around it. Test the selected representation, validator comparison, precedence, headers, and absent body as one evidence chain—not as five unrelated conveniences.
A 304 Is a Decision, Not a Smaller Payload
A normal 200 OK response to GET can carry the selected representation’s bytes. A 304 Not Modified response says something different: a conditional GET or HEAD has established that the client’s stored response can still be used. The useful body is already on the client side. The new response contributes status and metadata for identifying or updating that stored response.
That distinction gives the test oracle a clean first rule. RFC 9110 Section 15.4.5 says a 304 ends after its header section; it cannot contain content or trailers. A handler that changes statusCode to 304 and then calls the same “send the document” helper as the 200 branch has not produced a short success response. It has attempted to attach bytes to a response whose semantics forbid them.
The inverse failure matters too. “No body” does not mean “no useful fields.” A 304 has to carry applicable validator, selection, and cache metadata from the response it is validating. Tests therefore need to examine status, headers, and collected body bytes together. A green assertion on status === 304 is only the beginning.
Draw a Bright Boundary Around the Exercise
The fixture below is an origin server with one route, /note, and two representations. It handles only GET and HEAD. English is the default; a bounded Accept-Language selector can choose French. The server emits a strong, content-derived ETag, a fixed Last-Modified value, and Vary: Accept-Language. It then evaluates If-None-Match or, only when that field is absent, If-Modified-Since.
This is intentionally not a complete HTTP cache. It excludes unsafe write preconditions, range handling, compression-specific validators, and framework quirks; it neither implements nor tests cache storage, freshness calculations, or shared-cache behavior. The 405 case proves that an unsafe method cannot enter this read-only demonstration; it does not teach If-Match, lost-update prevention, or conditional writes. The cache-control value is observable fixture metadata, not a general deployment recommendation.
That scope is a feature. Generated code often looks plausible because it handles the happy path while silently combining concepts from neighboring protocol sections. A small contract lets a reviewer say exactly what passed and what remains untested.
Select the Representation Before You Validate It
A resource is not identical to the bytes returned for every request. Here, one URI can yield English or French representation data. The server first reads Accept-Language, chooses a language, encodes that body as UTF-8 bytes, and only then calculates its ETag and evaluates request preconditions. Reversing that order can validate the wrong variant.
The fixture makes the selection visible through Content-Language and Vary: Accept-Language. The Vary field tells a cache that this request field influenced representation selection. Under RFC 9111 Section 4.1, a stored response carrying Vary cannot simply be reused without revalidation unless the nominated request fields match the request that produced it. That is why the test matrix asks an English ETag to validate a French request and expects 200 with the French tag and bytes.
The selector understands this fixture’s two supported languages, basic language ranges such as fr-CA, quality values, declaration order, and a wildcard fallback. It is not presented as a general-purpose language negotiation library. The useful evidence is narrower: two different selected bodies produce two different ETags; quality ordering can choose English over French; and every 200 or 304 for this route names Accept-Language in Vary.
A Strong ETag Is a Promise About Change
RFC 9110 Section 8.8 defines validator fields and distinguishes strong from weak validators. A strong validator must change whenever a change to representation data would be observable in the content of a 200 response to GET. An ETag is opaque to the recipient: the client compares the quoted value; it does not need to understand whether the server used a revision number, file attributes, or a hash.
makeEtag() hashes the exact UTF-8 Buffer with SHA-256, encodes the digest as base64url, adds a stable sha256- label, and places the result inside double quotes. The response does not use the W/ prefix, so this is a strong claim. Within this bounded fixture, the representation metadata is fixed and every demonstrated data change changes the byte sequence and tag. In a wider system, a reviewer must also account for every representation-forming transformation and semantically significant metadata change before retaining that strong label.
The mutation test is deliberately behavioral. It stores the first English ETag, changes the English body, requests the route with the old tag, and requires a new 200, new bytes, new byte length, and a new ETag. Merely unit-testing that a hash function returns 43 base64url characters would miss the wiring error where the handler hashes a template, hashes before selection, or reuses a stale value.
If-None-Match Uses Weak Comparison
The response may advertise a strong ETag while If-None-Match still uses the weak comparison function. Under RFC 9110 Section 13.1.2, the recipient compares opaque tags character for character while ignoring whether either side has the W/ weakness marker. Thus a request containing W/"sha256-…" can match the current strong "sha256-…" response tag for GET or HEAD revalidation.
Do not turn that rule into loose string cleanup. The opaque value remains case-sensitive. The field can be a comma-separated list of entity tags, and any member can match. A field whose entire value is * is false as an If-None-Match condition when a current selected representation exists, so GET or HEAD receives 304. A generated implementation that compares only the complete raw header to the current ETag misses weak tags, lists, and the wildcard.
The fixture parses quoted opaque tags instead of blindly calling split(','), because a legal opaque tag can itself contain a comma. It accepts empty list members permitted by HTTP list syntax and treats malformed input as a non-match. It also respects the grammar that * stands alone; a star mixed into a tag list is not a valid generated field value. This parser is sufficient for the named evidence cases, not a claim of exhaustive hostile-input conformance.
Node does not supply these semantics automatically. Its HTTP API documentation explains that incoming header names are lower-cased and that the API parses messages into headers and bodies without parsing the actual header meanings. The application still owns entity-tag syntax, comparison mode, and precondition order.
An ETag Field Silences If-Modified-Since
The most valuable precedence test looks wrong at first glance. Send a nonmatching If-None-Match together with an If-Modified-Since date later than the fixture’s modification time. The date alone would lead to 304, but the combined request must return 200. Why? RFC 9110 requires a recipient to ignore If-Modified-Since whenever If-None-Match is present. The ETag condition is treated as the more accurate replacement.
This is not “ETag first, then maybe date.” It is “ETag only when supplied.” The server’s evaluatePreconditions() returns as soon as it sees If-None-Match, even when none of the listed tags match. That branch prevents a later date check from incorrectly converting a required 200 into 304.
When no ETag condition is present, If-Modified-Since is the fallback. An equal or later valid date means the selected representation has not been modified after the supplied time and produces 304. An older date produces 200. An invalid date is ignored and therefore also produces 200. These cases implement only steps 3 and 4—the If-None-Match and If-Modified-Since branches—of RFC 9110 Section 13.2.2. If-Match, If-Unmodified-Since, and Range/If-Range are explicitly excluded, even though the first two can also be used with GET or HEAD.
A Bodyless 304 Still Carries a Useful Header Set
For a 304, RFC 9110 requires any Content-Location, Date, ETag, and Vary fields that the same request’s 200 would have sent, plus applicable Cache-Control and Expires. The fixture sends ETag, Vary, and Cache-Control; Node adds Date. There is no Content-Location or Expires in either branch.
It also repeats Last-Modified, Content-Language, and Content-Type as metadata for the selected stored response. Those fields are not a universal mandatory-304 list. Section 15.4.5 advises against extra representation metadata unless it guides a cache update, so each production field should have a reason. The test asserts this fixture’s documented set without turning it into a general prescription.
Content-Length is deliberately absent from every 304. RFC 9110 permits it on a 304 response to conditional GET only when it equals the number of octets that the corresponding 200 would have carried, but omission avoids implying that a 304 has a body. The server calls response.end() with no data, and the client collector proves zero bytes arrived. Node’s documentation also requires response.end() on each response, including one with no body.
HEAD Reuses the Decision Path, Not the Body Path
RFC 9110 Section 9.3.2 defines HEAD as identical to GET except that the server must not send response content. The fixture consequently performs the same language selection, byte encoding, ETag calculation, and conditional evaluation for both methods. A normal HEAD returns 200 with the same ETag, language, and Content-Length that GET would produce, followed by zero body bytes.
A matching conditional HEAD returns 304 and remains bodyless. Although RFC 9110 notes that conditional HEAD usually saves little because an ordinary HEAD is already close in size to a 304, supporting the defined semantics keeps metadata probes consistent. Separate tests matter because generated handlers sometimes suppress the body for 200 HEAD but accidentally feed bytes into the shared 304 branch—or calculate HEAD metadata from an empty body.
The Complete Node 24 Fixture
Place the following three files in one empty directory. They use only built-in Node modules. Run npm run check for syntax checks, npm test for the evidence matrix, and npm start to inspect http://127.0.0.1:3000/note manually. The verified run used Node 24.18.0, the version named by the package’s engine field.
package.json
{
"name": "conditional-request-fixture",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node conditional-server.mjs",
"test": "node --test conditional-server.test.mjs",
"check": "node --check conditional-server.mjs && node --check conditional-server.test.mjs"
},
"engines": {
"node": "24.18.0"
}
}
conditional-server.mjs
import { createHash } from 'node:crypto';
import { createServer } from 'node:http';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
export const FIXED_LAST_MODIFIED = 'Thu, 03 Sep 2026 06:00:00 GMT';
const ORIGINAL_REPRESENTATIONS = Object.freeze({
en: [
'A 304 Is Not a Short 200.',
'',
'The selected representation lives in the cache; this response only confirms that its validator still matches.',
'',
].join('\n'),
fr: [
'Un 304 n’est pas un petit 200.',
'',
'La représentation sélectionnée reste dans le cache ; cette réponse confirme seulement que son validateur correspond encore.',
'',
].join('\n'),
});
export const representations = new Map(Object.entries(ORIGINAL_REPRESENTATIONS));
export function setRepresentation(language, body) {
if (!representations.has(language)) {
throw new RangeError(`Unsupported language: ${language}`);
}
if (typeof body !== 'string') {
throw new TypeError('Representation body must be a string');
}
representations.set(language, body);
}
export function resetRepresentations() {
for (const [language, body] of Object.entries(ORIGINAL_REPRESENTATIONS)) {
representations.set(language, body);
}
}
export function makeEtag(body) {
const digest = createHash('sha256').update(Buffer.from(body, 'utf8')).digest('base64url');
return `"sha256-${digest}"`;
}
export function selectLanguage(acceptLanguage = '') {
const preferences = String(acceptLanguage)
.split(',')
.map((part, order) => {
const [rawRange, ...parameters] = part.trim().split(';');
let quality = 1;
for (const parameter of parameters) {
const match = /^\s*q\s*=\s*(0(?:\.\d{0,3})?|1(?:\.0{0,3})?)\s*$/i.exec(parameter);
if (match) {
quality = Number(match[1]);
}
}
return { range: rawRange.toLowerCase(), quality, order };
})
.filter(({ range, quality }) => range && quality > 0)
.sort((left, right) => right.quality - left.quality || left.order - right.order);
for (const { range } of preferences) {
if (range === '*') return 'en';
if (range === 'fr' || range.startsWith('fr-')) return 'fr';
if (range === 'en' || range.startsWith('en-')) return 'en';
}
return 'en';
}
function readHeader(headers, requestedName) {
if (typeof headers?.get === 'function') {
const value = headers.get(requestedName);
return value === null ? undefined : value;
}
const key = Object.keys(headers ?? {}).find(
(candidate) => candidate.toLowerCase() === requestedName,
);
const value = key === undefined ? undefined : headers[key];
if (Array.isArray(value)) return value.join(', ');
return value === undefined ? undefined : String(value);
}
function parseEntityTagList(fieldValue) {
const input = String(fieldValue);
const tags = [];
let cursor = 0;
const skipWhitespace = () => {
while (input[cursor] === ' ' || input[cursor] === '\t') cursor += 1;
};
while (cursor < input.length) {
skipWhitespace();
// RFC list syntax permits empty members around commas.
if (input[cursor] === ',') {
cursor += 1;
continue;
}
let weak = false;
if (input.startsWith('W/', cursor)) {
weak = true;
cursor += 2;
}
if (input[cursor] !== '"') return [];
cursor += 1;
const opaqueStart = cursor;
while (cursor < input.length && input[cursor] !== '"') {
const codePoint = input.charCodeAt(cursor);
const valid = codePoint === 0x21
|| (codePoint >= 0x23 && codePoint <= 0x7e)
|| codePoint >= 0x80;
if (!valid) return [];
cursor += 1;
}
if (input[cursor] !== '"') return [];
const opaque = input.slice(opaqueStart, cursor);
cursor += 1;
tags.push({ weak, opaque });
skipWhitespace();
if (cursor < input.length) {
if (input[cursor] !== ',') return [];
cursor += 1;
}
}
return tags;
}
function ifNoneMatchMatches(fieldValue, currentEtag) {
if (String(fieldValue).trim() === '*') return true;
const [current] = parseEntityTagList(currentEtag);
if (!current) return false;
// If-None-Match uses weak comparison for GET and HEAD: W/ is ignored,
// while the opaque tag remains case-sensitive.
return parseEntityTagList(fieldValue).some(({ opaque }) => opaque === current.opaque);
}
export function evaluatePreconditions(headers, etag) {
const ifNoneMatch = readHeader(headers, 'if-none-match');
if (ifNoneMatch !== undefined) {
return ifNoneMatchMatches(ifNoneMatch, etag);
}
const ifModifiedSince = readHeader(headers, 'if-modified-since');
if (ifModifiedSince === undefined) return false;
const sinceTime = Date.parse(ifModifiedSince);
if (!Number.isFinite(sinceTime)) return false;
return Date.parse(FIXED_LAST_MODIFIED) <= sinceTime;
}
function writePlainResponse(response, method, statusCode, body, extraHeaders = {}) {
const bytes = Buffer.from(body, 'utf8');
response.writeHead(statusCode, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': bytes.length,
...extraHeaders,
});
response.end(method === 'HEAD' ? undefined : bytes);
}
export function createConditionalServer() {
return createServer((request, response) => {
const method = request.method ?? 'GET';
const pathname = new URL(request.url ?? '/', 'http://localhost').pathname;
if (pathname !== '/note') {
writePlainResponse(response, method, 404, 'Not Found\n');
return;
}
if (method !== 'GET' && method !== 'HEAD') {
writePlainResponse(response, method, 405, 'Method Not Allowed\n', { Allow: 'GET, HEAD' });
return;
}
const language = selectLanguage(request.headers['accept-language']);
const body = representations.get(language);
const bytes = Buffer.from(body, 'utf8');
const etag = makeEtag(body);
const representationHeaders = {
'Cache-Control': 'public, max-age=60, must-revalidate',
'Content-Language': language,
'Content-Type': 'text/plain; charset=utf-8',
ETag: etag,
'Last-Modified': FIXED_LAST_MODIFIED,
Vary: 'Accept-Language',
};
if (evaluatePreconditions(request.headers, etag)) {
response.writeHead(304, representationHeaders);
response.end();
return;
}
response.writeHead(200, {
...representationHeaders,
'Content-Length': bytes.length,
});
response.end(method === 'HEAD' ? undefined : bytes);
});
}
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : '';
if (invokedPath === import.meta.url) {
const host = process.env.HOST || '127.0.0.1';
const port = Number(process.env.PORT || 3000);
const server = createConditionalServer();
server.listen(port, host, () => {
const address = server.address();
const boundPort = typeof address === 'object' && address ? address.port : port;
console.log(`conditional server listening on http://${host}:${boundPort}/note`);
});
}
conditional-server.test.mjs
import assert from 'node:assert/strict';
import { request as httpRequest } from 'node:http';
import { after, before, test } from 'node:test';
import {
FIXED_LAST_MODIFIED,
createConditionalServer,
makeEtag,
representations,
resetRepresentations,
setRepresentation,
} from './conditional-server.mjs';
const server = createConditionalServer();
let origin;
const evidence = [];
before(async () => {
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
const { port } = server.address();
origin = { hostname: '127.0.0.1', port };
});
after(async () => {
resetRepresentations();
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
});
function send({ method = 'GET', path = '/note', headers = {} } = {}) {
return new Promise((resolve, reject) => {
const request = httpRequest({ ...origin, method, path, headers }, (response) => {
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
resolve({
status: response.statusCode,
headers: response.headers,
body: Buffer.concat(chunks),
});
});
});
request.once('error', reject);
request.end();
});
}
async function evidenceCase(t, label, check) {
await t.test(label, async () => {
await check();
evidence.push(label);
});
}
test('conditional request evidence matrix', async (t) => {
let english;
let french;
await evidenceCase(t, 'default GET returns the English 200 representation', async () => {
english = await send();
assert.equal(english.status, 200);
assert.equal(english.headers['content-language'], 'en');
assert.equal(english.headers.vary, 'Accept-Language');
assert.equal(english.headers['cache-control'], 'public, max-age=60, must-revalidate');
assert.equal(english.headers['last-modified'], FIXED_LAST_MODIFIED);
assert.equal(english.headers['content-type'], 'text/plain; charset=utf-8');
assert.match(english.headers.etag, /^"sha256-[A-Za-z0-9_-]{43}"$/);
assert.equal(Number(english.headers['content-length']), english.body.length);
assert.equal(english.body.toString('utf8'), representations.get('en'));
});
await evidenceCase(t, 'Accept-Language selects the French variant', async () => {
french = await send({ headers: { 'Accept-Language': 'fr-CA, en;q=0.5' } });
assert.equal(french.status, 200);
assert.equal(french.headers['content-language'], 'fr');
assert.notEqual(french.headers.etag, english.headers.etag);
assert.equal(Number(french.headers['content-length']), french.body.length);
assert.equal(french.body.toString('utf8'), representations.get('fr'));
});
await evidenceCase(t, 'quality values can prefer English over French', async () => {
const response = await send({ headers: { 'Accept-Language': 'fr;q=0.2, en;q=0.9' } });
assert.equal(response.headers['content-language'], 'en');
assert.equal(response.headers.etag, english.headers.etag);
});
await evidenceCase(t, 'HEAD has GET metadata and no body', async () => {
const response = await send({ method: 'HEAD' });
assert.equal(response.status, 200);
for (const header of [
'cache-control',
'content-language',
'content-type',
'etag',
'last-modified',
'vary',
'content-length',
]) {
assert.equal(response.headers[header], english.headers[header], header);
}
assert.equal(response.body.length, 0);
});
await evidenceCase(t, 'a matching strong If-None-Match returns 304', async () => {
const response = await send({ headers: { 'If-None-Match': english.headers.etag } });
assert.equal(response.status, 304);
assert.equal(response.body.length, 0);
assert.equal(response.headers.etag, english.headers.etag);
assert.equal(response.headers.vary, 'Accept-Language');
assert.equal(response.headers['cache-control'], 'public, max-age=60, must-revalidate');
assert.equal(response.headers['content-language'], 'en');
assert.equal(response.headers['content-type'], english.headers['content-type']);
assert.equal(response.headers['last-modified'], english.headers['last-modified']);
assert.match(response.headers.date, /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/);
assert.equal(new Date(response.headers.date).toUTCString(), response.headers.date);
assert.equal(response.headers['content-length'], undefined);
});
await evidenceCase(t, 'weak If-None-Match comparison matches a strong ETag', async () => {
const response = await send({ headers: { 'If-None-Match': `W/${english.headers.etag}` } });
assert.equal(response.status, 304);
assert.equal(response.body.length, 0);
});
await evidenceCase(t, 'an ETag list matches any member', async () => {
const value = `"old,still-old", W/${english.headers.etag}, "other"`;
const response = await send({ headers: { 'If-None-Match': value } });
assert.equal(response.status, 304);
});
await evidenceCase(t, 'If-None-Match wildcard matches the selected representation', async () => {
const response = await send({ headers: { 'If-None-Match': '*' } });
assert.equal(response.status, 304);
});
await evidenceCase(t, 'a nonmatching If-None-Match returns 200', async () => {
const response = await send({ headers: { 'If-None-Match': '"not-current"' } });
assert.equal(response.status, 200);
assert.deepEqual(response.body, english.body);
});
await evidenceCase(t, 'If-None-Match takes precedence over a future date', async () => {
const response = await send({ headers: {
'If-None-Match': '"not-current"',
'If-Modified-Since': 'Fri, 04 Sep 2026 06:00:00 GMT',
} });
assert.equal(response.status, 200);
});
await evidenceCase(t, 'an equal If-Modified-Since date returns 304', async () => {
const response = await send({ headers: { 'If-Modified-Since': FIXED_LAST_MODIFIED } });
assert.equal(response.status, 304);
assert.equal(response.body.length, 0);
});
await evidenceCase(t, 'a future If-Modified-Since date returns 304', async () => {
const response = await send({ headers: { 'If-Modified-Since': 'Fri, 04 Sep 2026 06:00:00 GMT' } });
assert.equal(response.status, 304);
});
await evidenceCase(t, 'an older If-Modified-Since date returns 200', async () => {
const response = await send({ headers: { 'If-Modified-Since': 'Wed, 02 Sep 2026 06:00:00 GMT' } });
assert.equal(response.status, 200);
});
await evidenceCase(t, 'an invalid If-Modified-Since date is ignored', async () => {
const response = await send({ headers: { 'If-Modified-Since': 'not-a-date' } });
assert.equal(response.status, 200);
});
await evidenceCase(t, 'an English validator does not validate the French variant', async () => {
const response = await send({ headers: {
'Accept-Language': 'fr',
'If-None-Match': english.headers.etag,
} });
assert.equal(response.status, 200);
assert.equal(response.headers.etag, french.headers.etag);
assert.deepEqual(response.body, french.body);
});
await evidenceCase(t, 'conditional HEAD returns a bodyless 304', async () => {
const response = await send({ method: 'HEAD', headers: { 'If-None-Match': english.headers.etag } });
assert.equal(response.status, 304);
assert.equal(response.headers.etag, english.headers.etag);
assert.equal(response.body.length, 0);
});
await evidenceCase(t, 'mutating bytes changes the strong ETag', async () => {
const changedBody = `${representations.get('en')}One changed byte sequence.\n`;
const predictedEtag = makeEtag(changedBody);
setRepresentation('en', changedBody);
try {
const response = await send({ headers: { 'If-None-Match': english.headers.etag } });
assert.equal(response.status, 200);
assert.equal(response.body.toString('utf8'), changedBody);
assert.equal(response.headers.etag, predictedEtag);
assert.notEqual(response.headers.etag, english.headers.etag);
assert.equal(Number(response.headers['content-length']), Buffer.byteLength(changedBody));
} finally {
resetRepresentations();
}
});
await evidenceCase(t, 'an unknown path is 404', async () => {
const response = await send({ path: '/missing' });
assert.equal(response.status, 404);
});
await evidenceCase(t, 'unsafe methods are rejected without mutation', async () => {
const before = representations.get('en');
const response = await send({ method: 'POST' });
assert.equal(response.status, 405);
assert.equal(response.headers.allow, 'GET, HEAD');
assert.equal(representations.get('en'), before);
});
console.log('\nEvidence matrix');
evidence.forEach((label, index) => {
console.log(`${String(index + 1).padStart(2, '0')} PASS ${label}`);
});
console.log(`Summary: ${evidence.length}/${evidence.length} evidence cases passed`);
});
Turn the Evidence Matrix Into a Release Gate
The test file reports nineteen named evidence cases inside one parent test. Node therefore reports twenty passing tests: nineteen subtests plus their parent. Preserve that distinction in build logs; “20 tests” should not quietly become “20 independent protocol scenarios.”
Release this bounded validator path only when all rows pass
- Representation rows: default English GET and negotiated French GET return their complete expected UTF-8 bodies, language metadata, distinct tags, and
Content-Lengthvalues equal to collected byte length; q-values can prefer English. - HEAD row: an unconditional HEAD matches GET’s documented
Cache-Control,Content-Language,Content-Type, ETag,Last-Modified,Vary, andContent-Lengthvalues while transferring zero bytes. - ETag rows: exact, weak, list-member with a legal quoted-comma decoy, and wildcard matches return bodyless 304; a miss returns the full 200 representation.
- Precedence row: a present but nonmatching ETag overrides a later modification date and leaves the response at 200.
- Date rows: equal and future valid dates return 304; older and invalid dates return 200.
- Variant row: the English validator cannot validate the French selection.
- Conditional HEAD row: a matching HEAD condition returns 304 with no body.
- Mutation row: changed English bytes invalidate the prior tag and produce a new 200 body, length, and ETag.
- Boundary rows: an unknown route returns 404, and POST returns 405 with
Allow: GET, HEADwithout changing state.
Hold the release when a result is ambiguous
- Hold if a 304 carries any collected body byte or omits the selected representation’s ETag or
Varyfield. - Hold if English and French share a validator despite different bytes, or if an English tag validates a French response.
- Hold if
If-Modified-Sincechanges the result of any request that already containsIf-None-Match. - Hold if HEAD metadata is calculated from an empty payload instead of the representation GET would have selected.
- Hold if the production representation pipeline has transformations or metadata variation that the ETag input does not cover.
- Hold if the production modification timestamp can remain unchanged while clients are expected to rely on date-only revalidation.
Interpret 19/19 Without Expanding the Claim
A passing run establishes a reproducible result for this server, this route, these two in-memory representations, and this Node version. It shows that the origin chooses the tested variant before validation; its demonstrated byte mutations change the strong tag; the tested ETag forms use weak comparison; ETag precedence beats the date fallback; 304 and HEAD transfer no content; HEAD matches the complete documented representation header set and length; and the tested 304 repeats the documented stable metadata while carrying a valid Date.
The suite uses Node’s low-level HTTP client rather than a browser or fetch(). That keeps an implicit client cache out of the oracle. Each response is collected into a Buffer, so “bodyless” means a measured length of zero. It measures both English and French Content-Length values against collected byte length rather than JavaScript character count; the French assertion therefore covers multibyte UTF-8 text.
It does not prove that an intermediary will store, choose, freshen, or evict responses correctly. RFC 9111 Section 4.3 describes revalidation as a cache forwarding a conditional request so an inbound server can confirm a stored response, update its metadata, or replace it. This fixture verifies the origin-facing answer to that exchange; no shared cache participates.
Keep the Limitations Beside the Green Check
- Language negotiation is deliberately small. It supports English, French, basic prefixes, simple q-values, and a default. It is not an exhaustive implementation of every language-range and malformed-field edge case.
- Date parsing is pragmatic.
Date.parse()can accept strings beyond the strict HTTP-date grammar. The matrix uses valid IMF-fixdate examples plus one obvious invalid value; it is not a date-parser conformance suite. - The modification time is fixed. The mutation hook changes bytes and ETag but not
Last-Modified. That isolates ETag evidence, but a production update pipeline that advertises Last-Modified must advance it consistently. Date-only validation after the synthetic mutation is outside the earned claim. - The entity-tag parser is bounded. It handles the demonstrated strong, weak, list, wildcard, whitespace, and legal quoted-comma-decoy cases. Malformed tag fields become non-matches rather than a catalog of every possible recipient behavior.
- The strong tag covers final body bytes in a fixed metadata profile. Compression-specific variants, ranges, dynamically changing media types, and other representation transformations are excluded.
- The cache directive is illustrative. The exercise does not evaluate authorization, privacy, shared-cache eligibility, freshness calculations, stale serving, eviction, or request collapsing.
- Write safety is not in scope. POST is rejected; no conclusion should be drawn about
If-Match,If-Unmodified-Since, 412 responses, concurrent updates, or idempotency. - No framework sits between the code and Node. Middleware that generates ETags, compresses responses, strips headers, or rewrites HEAD/304 behavior needs a separate integration matrix.
Use AI to Draft Tests, Then Make a Human Own the Protocol Claim
An AI assistant is useful for enumerating branches, drafting a fixture, and turning RFC language into candidate assertions. It is also capable of producing a beautifully factored helper whose meaning is wrong: a strict ETag comparison in If-None-Match, a date check after an ETag miss, a validator calculated before content negotiation, or a 304 routed through a 200 body writer.
The responsible review move is not to guess whether the code “looks AI-written.” Give one engineer ownership of the HTTP claim. Have that reviewer read the cited sections, define the representation bytes, extract the published fixture mechanically, run the exact versioned commands, inspect the raw status/headers/body, and record both the release rows and the exclusions. If production adds a variant or transformation, add the failing test first and reconsider whether the validator remains strong.
Keep editorial changes separate from protocol changes. A rewrite can clarify why 304 differs from 200, but it must not silently change header names, comparison rules, dates, expected statuses, test labels, code, or citations. Natural prose is valuable only while the executable contract remains intact.
Humanize the Explanation, Preserve the Evidence
Use the AI Humanizer to refine the article around your verified implementation, then compare the result against the RFC links and the passing fixture. Lock every code block, status code, validator example, header name, limitation, and release outcome before rewriting. A human reviewer remains responsible for the protocol and the deployment decision.
Try the AI Humanizer, Then Re-run the Tests ->