Conformance
Results from the cache-tests suite. 284 of 365 tests pass, and each of the other 81 has a written reason.
"RFC compliant" is easy to write. This page is what is behind it for Weir: the http-tests/cache-tests suite, run against examples/weirproxy with default settings.
Tests in the suite (commit d644cf4) | 365 |
| Pass | 284 |
| Fail, each explained below | 81 |
The suite's authors say that passing everything means nothing: many tests check optional behavior, and some check behavior a careful shared cache should refuse. Most of Weir's 81 are deliberate. Weir ignores client no-cache by default, refuses to store Set-Cookie responses, does not store 206 responses, and treats a malformed max-age as zero. Each row below gives the reason and the decision or requirement behind it.
A nightly job reruns the suite and fails only when a test that passed in this baseline stops passing. Reproduce it with make cache-tests.
RFCs implemented
| RFC | Subject |
|---|---|
| 9111 | HTTP caching (the shared-cache half) |
| 9110 | HTTP semantics: conditional requests, ranges, Vary |
| 9211 | the Cache-Status header |
| 9213 | targeted cache-control (CDN-Cache-Control) |
| 9875 | cache groups and group invalidation |
| 5861 | stale-while-revalidate and stale-if-error |
The requirement-by-requirement mapping is in section 8 of the technical specification.
The 81 expected failures
Rendered from docs/cache-tests-expected-failures.md. The reasons were read off the requirements and the suite's request logs. They were not each traced through the code.
By design
| Tests | Why Weir differs | Decision |
|---|---|---|
ccreq-ma0, ccreq-ma1, ccreq-magreaterage, ccreq-max-stale, ccreq-max-stale-age, ccreq-min-fresh, ccreq-min-fresh-age, ccreq-no-cache, ccreq-no-cache-etag, ccreq-no-cache-lm | Client request directives that force revalidation are ignored unless Client.HonorRevalidation is set | D5, FR-SRV-8 |
ccreq-no-store | Request no-store stops the response being stored; it does not bypass a stored entry | FR-SRV-7 |
stale-503, stale-close | Stale is served only when the origin permits it (stale-if-error), and only for the error conditions in FR-STL-2 (a 503 or a closed connection with max-age=2 alone is not one) | D6, FR-STL-2 |
stale-warning-become, stale-warning-stored | The setup needs a stale reuse the origin did not permit; Warning is also obsolete in RFC 9111 | D6, D7 |
status-299-fresh, status-303-fresh, status-400-fresh, status-499-fresh, status-500-fresh, status-502-fresh, status-503-fresh, status-504-fresh, status-599-fresh, heuristic-599-cached | Only the statuses in Storable.Statuses are stored | FR-STO-2, D39 |
method-POST, head-200-freshness-update, head-200-retain, head-200-update, head-410-update, head-writethrough | Only a forwarded GET response is stored, so a POST or HEAD response never updates an entry | FR-STO-1 |
headers-store-Set-Cookie, other-set-cookie | A response with Set-Cookie is not stored unless Storable.StripSetCookie is set | FR-STO-6, D31 |
conditional-etag-forward, conditional-etag-forward-unquoted, conditional-etag-vary-headers-mismatch | Client If-None-Match is not forwarded on a cacheable request; only the keyed, allowed and protocol headers are | D4 |
304-etag-update-response-Content-Encoding, 304-etag-update-response-Content-Type | A 304 does not change Content-Encoding or Content-Type: they describe the stored body | FR-SRV-3 |
304-etag-update-response-Set-Cookie | The setup response carries Set-Cookie, so it is never stored | FR-STO-6, D31 |
freshness-max-age-100a, freshness-max-age-a100, freshness-max-age-decimal-five, freshness-max-age-decimal-zero, freshness-max-age-two-fresh-stale-sameline, freshness-max-age-two-fresh-stale-sepline, freshness-max-age-two-stale-fresh-sameline, freshness-max-age-two-stale-fresh-sepline | A non-integer max-age, or one repeated with different values, makes the lifetime zero | FR-FRS-2 |
freshness-max-age-date | Age ignores apparent_age, so a skewed origin Date does not turn the cache off | FR-FRS-4, T-30 |
freshness-expires-wrong-case-tz | HTTP dates are case-sensitive; gMT is an invalid Expires, a time in the past | FR-FRS-2 |
heuristic-delta-5, heuristic-delta-10, heuristic-delta-30 | The heuristic lifetime is 10% of Date minus Last-Modified, so 0.5 to 3 seconds here | FR-FRS-3 |
other-age-delay | Age is added to responses served from a stored entry, not to the response that triggered the fetch | FR-FRS-7 |
conditional-etag-quoted-respond-unquoted, conditional-etag-unquoted-respond-quoted, conditional-etag-unquoted-respond-unquoted, conditional-etag-strong-generate-unquoted, conditional-etag-strong-respond-obs-text, conditional-etag-weak-respond-backslash, conditional-etag-weak-respond-lowercase, conditional-etag-weak-respond-omit-slash | Entity tags are compared as RFC 9110 §8.8.3 defines them; unquoted, mis-cased or malformed tags never match | FR-SRV-2, FR-SRV-3 |
age-parse-numeric-parameter, age-parse-parameter | An Age with parameters (7200;foo=bar) is not a delta-seconds value, so it is ignored (age 0) as RFC 9111 §5.1 says for an invalid Age | FR-FRS-4, RFC 9111 §5.1 |
headers-omit-headers-listed-in-Cache-Control-no-cache, headers-omit-headers-listed-in-Cache-Control-no-cache-single | A qualified no-cache="a" is read as unqualified (validate before every reuse), the same strict reading as qualified private | FR-STO-4, FR-SRV-1 |
vary-normalise-space | A keyed header value is compared exactly; 1,2 and 1, 2 are two variants. An "optimal" test; normalizing would be a key-boundary change | FR-KEY-9, P2 |
interim-102, interim-103, interim-no-header-reuse, interim-not-cached | 1xx responses are not relayed to the client (the final response is cached correctly). Relaying is not a Phase 1 goal | D43 |
headers-store-Transfer-Encoding | The suite sends an invalid Transfer-Encoding value and Go's HTTP client rejects the response, so Weir answers 502 for a malformed origin response | FR-FWD-7, ErrOrigin |
304-etag-update-response-ETag | The suite reports a retry (↻), which its authors say needs manual interpretation; nothing is asserted | no assertion to decide (suite limitation) |
conditional-lm-fresh-no-lm | The suite sends If-Modified-Since 3000 s before the response's Date and the response has no Last-Modified. FR-SRV-2 compares the stored Date, which is 3000 s later than the client's If-Modified-Since, so the resource counts as modified and the answer is 200 (RFC 9110 §13.1.3). The test expects 304 | FR-SRV-2 |
pragma-response-no-cache-heuristic | Go's net/http client rewrites a response Pragma: no-cache without Cache-Control into Cache-Control: no-cache before Weir sees it (fixPragmaCacheControl), so the engine reads a real no-cache and validates. The error is on the safe side, and telling the two apart would mean parsing responses outside the standard library | FR-FRS-3, RFC 9111 §5.4 |
Targeted fields and partial content
| Tests | Why Weir differs | Decision |
|---|---|---|
cdn-fresh-cc-nostore | An "optimal" test: it expects CDN-Cache-Control: max-age to override Cache-Control: no-store. Weir lets no-store, private and no-cache bind from either field, so a per-user page marked only in Cache-Control is never shared | D12, FR-TCC-3, T-34 |
cdn-max-age-case-insensitive | CDN-Cache-Control is an RFC 9651 Dictionary, whose keys are lowercase; MaX-aGe is not a valid key, so the field is ignored (RFC 9213 §2.1) | D12, FR-TCC-1 |
partial-store-partial-complete, partial-store-partial-reuse-partial, partial-store-partial-reuse-partial-absent, partial-store-partial-reuse-partial-byterange, partial-store-partial-reuse-partial-suffix | Storing a 206 response is a non-goal: Weir serves ranges from complete objects only | D11, FR-STO-2 |