diff --git a/test/fixtures/cache-tests/results/apache.json b/test/fixtures/cache-tests/results/apache.json index 86c732f6e36..5da2d1df2d9 100644 --- a/test/fixtures/cache-tests/results/apache.json +++ b/test/fixtures/cache-tests/results/apache.json @@ -423,6 +423,10 @@ "heuristic-delta-60": true, "heuristic-delta-600": true, "heuristic-delta-86400": true, + "interim-102": true, + "interim-103": true, + "interim-no-header-reuse": true, + "interim-not-cached": true, "invalidate-DELETE": true, "invalidate-DELETE-cl": [ "Assertion", diff --git a/test/fixtures/cache-tests/results/caddy.json b/test/fixtures/cache-tests/results/caddy.json index d45ef15d4a9..b9bcb867da0 100644 --- a/test/fixtures/cache-tests/results/caddy.json +++ b/test/fixtures/cache-tests/results/caddy.json @@ -537,6 +537,22 @@ "Assertion", "Response 2 does not come from cache" ], + "interim-102": [ + "Assertion", + "Interim response 1 not received" + ], + "interim-103": [ + "Assertion", + "Interim response 1 not received" + ], + "interim-no-header-reuse": [ + "Assertion", + "Interim response 1 not received" + ], + "interim-not-cached": [ + "Assertion", + "Interim response 1 not received" + ], "invalidate-DELETE": true, "invalidate-DELETE-cl": [ "Assertion", diff --git a/test/fixtures/cache-tests/results/haproxy.json b/test/fixtures/cache-tests/results/haproxy.json new file mode 100644 index 00000000000..9b30c981005 --- /dev/null +++ b/test/fixtures/cache-tests/results/haproxy.json @@ -0,0 +1,859 @@ +{ + "304-etag-update-response-Cache-Control": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Clear-Site-Data": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Content-Encoding": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Content-Foo": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Content-Length": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Content-Location": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Content-MD5": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Content-Range": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Content-Security-Policy": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Content-Type": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-ETag": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Expires": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Public-Key-Pins": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Set-Cookie": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Set-Cookie2": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-Test-Header": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-X-Content-Foo": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-X-Frame-Options": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-X-Test-Header": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-etag-update-response-X-XSS-Protection": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "304-lm-use-stored-Test-Header": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "age-parse-dup-0": [ + "Assertion", + "Response 2 does not come from cache" + ], + "age-parse-dup-0-twoline": [ + "Assertion", + "Response 2 does not come from cache" + ], + "age-parse-dup-old": true, + "age-parse-float": [ + "Assertion", + "Response 2 does not come from cache" + ], + "age-parse-large": true, + "age-parse-large-minus-one": true, + "age-parse-larger": true, + "age-parse-negative": [ + "Assertion", + "Response 2 does not come from cache" + ], + "age-parse-nonnumeric": [ + "Assertion", + "Response 2 does not come from cache" + ], + "age-parse-numeric-parameter": true, + "age-parse-parameter": true, + "age-parse-prefix": [ + "Assertion", + "Response 2 does not come from cache" + ], + "age-parse-prefix-twoline": [ + "Assertion", + "Response 2 does not come from cache" + ], + "age-parse-suffix": true, + "age-parse-suffix-twoline": true, + "cc-resp-must-revalidate-fresh": true, + "cc-resp-must-revalidate-stale": [ + "Assertion", + "Request 3 should have been conditional, but it was not." + ], + "cc-resp-no-cache": true, + "cc-resp-no-cache-case-insensitive": true, + "cc-resp-no-cache-revalidate": [ + "Assertion", + "Request 2 should have been conditional, but it was not." + ], + "cc-resp-no-cache-revalidate-fresh": [ + "Assertion", + "Request 2 should have been conditional, but it was not." + ], + "cc-resp-no-store": true, + "cc-resp-no-store-case-insensitive": true, + "cc-resp-no-store-fresh": true, + "cc-resp-no-store-old-max-age": true, + "cc-resp-no-store-old-new": true, + "cc-resp-private-shared": true, + "ccreq-ma0": true, + "ccreq-ma1": true, + "ccreq-magreaterage": [ + "Setup", + "Response 1 header Age is \"null\", not \"1800\"" + ], + "ccreq-max-stale": [ + "Assertion", + "Response 2 does not come from cache" + ], + "ccreq-max-stale-age": [ + "Assertion", + "Response 2 does not come from cache" + ], + "ccreq-min-fresh": true, + "ccreq-min-fresh-age": [ + "Setup", + "Response 1 header Age is \"null\", not \"1000\"" + ], + "ccreq-no-cache": true, + "ccreq-no-cache-etag": [ + "Assertion", + "Request 2 should have been conditional, but it was not." + ], + "ccreq-no-cache-lm": [ + "Assertion", + "Request 2 should have been conditional, but it was not." + ], + "ccreq-no-store": [ + "Assertion", + "Response 2 comes from cache" + ], + "ccreq-oic": [ + "Assertion", + "Response 1 status is 200, not 504" + ], + "cdn-cc-invalid-sh-type-unknown": true, + "cdn-cc-invalid-sh-type-wrong": [ + "AbortError", + "This operation was aborted" + ], + "cdn-date-update-exceed": true, + "cdn-expires-update-exceed": [ + "Assertion", + "Response 2 header Expires is \"null\", not \"Wed, 12 Mar 2025 10:13:52 GMT\"" + ], + "cdn-fresh-cc-nostore": [ + "Assertion", + "Response 2 does not come from cache" + ], + "cdn-max-age": [ + "Assertion", + "Response 2 does not come from cache" + ], + "cdn-max-age-0": true, + "cdn-max-age-0-expires": [ + "Assertion", + "Response 2 comes from cache" + ], + "cdn-max-age-age": true, + "cdn-max-age-case-insensitive": [ + "Assertion", + "Response 2 does not come from cache" + ], + "cdn-max-age-cc-max-age-invalid-expires": [ + "Assertion", + "Response 2 does not come from cache" + ], + "cdn-max-age-expires": [ + "Assertion", + "Response 2 does not come from cache" + ], + "cdn-max-age-extension": [ + "Assertion", + "Response 2 does not come from cache" + ], + "cdn-max-age-long-cc-max-age": [ + "Assertion", + "Response 2 comes from cache" + ], + "cdn-max-age-max": [ + "Assertion", + "Response 2 does not come from cache" + ], + "cdn-max-age-max-plus": [ + "Assertion", + "Response 2 does not come from cache" + ], + "cdn-max-age-short-cc-max-age": [ + "Assertion", + "Response 2 does not come from cache" + ], + "cdn-max-age-space-after-equals": true, + "cdn-max-age-space-before-equals": true, + "cdn-no-cache": [ + "Assertion", + "Response 2 comes from cache" + ], + "cdn-no-store-cc-fresh": [ + "Assertion", + "Response 2 comes from cache" + ], + "cdn-private": [ + "Assertion", + "Response 2 comes from cache" + ], + "cdn-remove-age-exceed": [ + "Assertion", + "Response 2 Age header not present." + ], + "cdn-remove-header": true, + "conditional-304-etag": true, + "conditional-etag-forward": true, + "conditional-etag-forward-unquoted": [ + "Assertion", + "Request 1 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" + ], + "conditional-etag-precedence": true, + "conditional-etag-quoted-respond-unquoted": [ + "Assertion", + "Response 2 status is 200, not 304" + ], + "conditional-etag-strong-generate": [ + "Assertion", + "Request 2 should have been conditional, but it was not." + ], + "conditional-etag-strong-generate-unquoted": [ + "Assertion", + "Request 2 should have been conditional, but it was not." + ], + "conditional-etag-strong-respond": true, + "conditional-etag-strong-respond-multiple-first": true, + "conditional-etag-strong-respond-multiple-last": true, + "conditional-etag-strong-respond-multiple-second": true, + "conditional-etag-strong-respond-obs-text": [ + "Assertion", + "Response 2 status is 200, not 304" + ], + "conditional-etag-unquoted-respond-quoted": [ + "Assertion", + "Response 2 status is 200, not 304" + ], + "conditional-etag-unquoted-respond-unquoted": [ + "Assertion", + "Response 2 status is 200, not 304" + ], + "conditional-etag-vary-headers": [ + "Setup", + "Request 2 should have been conditional, but it was not." + ], + "conditional-etag-vary-headers-mismatch": true, + "conditional-etag-weak-generate-weak": [ + "Assertion", + "Request 2 should have been conditional, but it was not." + ], + "conditional-etag-weak-respond": true, + "conditional-etag-weak-respond-backslash": [ + "Assertion", + "Response 2 status is 200, not 304" + ], + "conditional-etag-weak-respond-lowercase": [ + "Assertion", + "Response 2 status is 200, not 304" + ], + "conditional-etag-weak-respond-omit-slash": [ + "Assertion", + "Response 2 status is 200, not 304" + ], + "conditional-lm-fresh": true, + "conditional-lm-fresh-earlier": true, + "conditional-lm-fresh-no-lm": [ + "Assertion", + "Response 2 status is 200, not 304" + ], + "conditional-lm-fresh-rfc850": true, + "conditional-lm-stale": true, + "freshness-expires-32bit": true, + "freshness-expires-age-fast-date": [ + "Assertion", + "Response 2 comes from cache" + ], + "freshness-expires-age-slow-date": true, + "freshness-expires-ansi-c": true, + "freshness-expires-far-future": true, + "freshness-expires-future": true, + "freshness-expires-invalid": true, + "freshness-expires-invalid-1-digit-hour": true, + "freshness-expires-invalid-2-digit-year": true, + "freshness-expires-invalid-aest": true, + "freshness-expires-invalid-date": true, + "freshness-expires-invalid-date-dashes": true, + "freshness-expires-invalid-multiple-lines": true, + "freshness-expires-invalid-multiple-spaces": true, + "freshness-expires-invalid-no-comma": true, + "freshness-expires-invalid-time-periods": true, + "freshness-expires-invalid-utc": true, + "freshness-expires-old-date": [ + "Assertion", + "Response 2 comes from cache" + ], + "freshness-expires-past": true, + "freshness-expires-present": true, + "freshness-expires-rfc850": true, + "freshness-expires-wrong-case-month": [ + "Assertion", + "Response 2 does not come from cache" + ], + "freshness-expires-wrong-case-tz": [ + "Assertion", + "Response 2 does not come from cache" + ], + "freshness-expires-wrong-case-weekday": [ + "Assertion", + "Response 2 does not come from cache" + ], + "freshness-max-age": true, + "freshness-max-age-0": true, + "freshness-max-age-0-expires": true, + "freshness-max-age-100a": true, + "freshness-max-age-a100": [ + "Assertion", + "Response 2 does not come from cache" + ], + "freshness-max-age-age": true, + "freshness-max-age-case-insenstive": true, + "freshness-max-age-date": [ + "Assertion", + "Response 2 comes from cache" + ], + "freshness-max-age-decimal-five": true, + "freshness-max-age-decimal-zero": true, + "freshness-max-age-expires": true, + "freshness-max-age-expires-invalid": true, + "freshness-max-age-extension": true, + "freshness-max-age-ignore-quoted": true, + "freshness-max-age-ignore-quoted-rev": true, + "freshness-max-age-leading-zero": true, + "freshness-max-age-max": true, + "freshness-max-age-max-minus-1": true, + "freshness-max-age-max-plus": true, + "freshness-max-age-max-plus-1": true, + "freshness-max-age-negative": true, + "freshness-max-age-quoted": true, + "freshness-max-age-s-maxage-shared-longer": true, + "freshness-max-age-s-maxage-shared-longer-multiple": true, + "freshness-max-age-s-maxage-shared-longer-reversed": true, + "freshness-max-age-s-maxage-shared-shorter": true, + "freshness-max-age-s-maxage-shared-shorter-expires": true, + "freshness-max-age-single-quoted": true, + "freshness-max-age-space-after-equals": [ + "Assertion", + "Response 2 comes from cache" + ], + "freshness-max-age-space-before-equals": [ + "Assertion", + "Response 2 comes from cache" + ], + "freshness-max-age-stale": true, + "freshness-max-age-two-fresh-stale-sameline": [ + "Assertion", + "Response 2 does not come from cache" + ], + "freshness-max-age-two-fresh-stale-sepline": [ + "Assertion", + "Response 2 does not come from cache" + ], + "freshness-max-age-two-stale-fresh-sameline": true, + "freshness-max-age-two-stale-fresh-sepline": true, + "freshness-none": true, + "freshness-s-maxage-shared": true, + "head-200-freshness-update": [ + "Assertion", + "Response 3 does not come from cache" + ], + "head-200-retain": [ + "Assertion", + "Response 2 header Template-A is \"null\", not \"1\"" + ], + "head-200-update": [ + "Setup", + "Response 3 does not come from cache" + ], + "head-410-update": [ + "Setup", + "Response 3 does not come from cache" + ], + "head-writethrough": true, + "headers-omit-headers-listed-in-Cache-Control-no-cache": [ + "Assertion", + "Response 2 includes unexpected header a: \"1\"" + ], + "headers-omit-headers-listed-in-Cache-Control-no-cache-single": [ + "Assertion", + "Response 2 includes unexpected header a: \"1\"" + ], + "headers-omit-headers-listed-in-Connection": [ + "Assertion", + "Response 2 includes unexpected header a: \"1\"" + ], + "headers-store-Cache-Control": true, + "headers-store-Clear-Site-Data": true, + "headers-store-Connection": true, + "headers-store-Content-Encoding": true, + "headers-store-Content-Foo": true, + "headers-store-Content-Length": true, + "headers-store-Content-Location": true, + "headers-store-Content-MD5": true, + "headers-store-Content-Range": true, + "headers-store-Content-Security-Policy": true, + "headers-store-Content-Type": true, + "headers-store-ETag": true, + "headers-store-Expires": true, + "headers-store-Keep-Alive": true, + "headers-store-Proxy-Authenticate": true, + "headers-store-Proxy-Authentication-Info": true, + "headers-store-Proxy-Authorization": true, + "headers-store-Proxy-Connection": true, + "headers-store-Public-Key-Pins": true, + "headers-store-Set-Cookie": true, + "headers-store-Set-Cookie2": true, + "headers-store-TE": true, + "headers-store-Test-Header": true, + "headers-store-Transfer-Encoding": [ + "Setup", + "Response 1 status is 502, not 200" + ], + "headers-store-Upgrade": true, + "headers-store-X-Content-Foo": true, + "headers-store-X-Frame-Options": true, + "headers-store-X-Test-Header": true, + "headers-store-X-XSS-Protection": true, + "heuristic-200-cached": true, + "heuristic-201-not_cached": true, + "heuristic-202-not_cached": true, + "heuristic-203-cached": [ + "Assertion", + "Response 2 does not come from cache" + ], + "heuristic-204-cached": [ + "Assertion", + "Response 2 does not come from cache" + ], + "heuristic-403-not_cached": true, + "heuristic-404-cached": [ + "Assertion", + "Response 2 does not come from cache" + ], + "heuristic-405-cached": [ + "Assertion", + "Response 2 does not come from cache" + ], + "heuristic-410-cached": [ + "Assertion", + "Response 2 does not come from cache" + ], + "heuristic-414-cached": [ + "Assertion", + "Response 2 does not come from cache" + ], + "heuristic-501-cached": [ + "Assertion", + "Response 2 does not come from cache" + ], + "heuristic-502-not_cached": true, + "heuristic-503-not_cached": true, + "heuristic-504-not_cached": true, + "heuristic-599-cached": [ + "Assertion", + "Response 2 does not come from cache" + ], + "heuristic-599-not_cached": true, + "heuristic-delta-10": true, + "heuristic-delta-1200": true, + "heuristic-delta-1800": true, + "heuristic-delta-30": true, + "heuristic-delta-300": true, + "heuristic-delta-3600": true, + "heuristic-delta-43200": true, + "heuristic-delta-5": true, + "heuristic-delta-60": true, + "heuristic-delta-600": true, + "heuristic-delta-86400": true, + "interim-102": true, + "interim-103": true, + "interim-no-header-reuse": true, + "interim-not-cached": true, + "invalidate-DELETE": true, + "invalidate-DELETE-cl": [ + "Assertion", + "Response 3 comes from cache" + ], + "invalidate-DELETE-failed": true, + "invalidate-DELETE-location": [ + "Assertion", + "Response 3 comes from cache" + ], + "invalidate-M-SEARCH": true, + "invalidate-M-SEARCH-cl": [ + "Assertion", + "Response 3 comes from cache" + ], + "invalidate-M-SEARCH-failed": true, + "invalidate-M-SEARCH-location": [ + "Assertion", + "Response 3 comes from cache" + ], + "invalidate-POST": true, + "invalidate-POST-cl": [ + "Assertion", + "Response 3 comes from cache" + ], + "invalidate-POST-failed": true, + "invalidate-POST-location": [ + "Assertion", + "Response 3 comes from cache" + ], + "invalidate-PUT": true, + "invalidate-PUT-cl": [ + "Assertion", + "Response 3 comes from cache" + ], + "invalidate-PUT-failed": true, + "invalidate-PUT-location": [ + "Assertion", + "Response 3 comes from cache" + ], + "method-POST": [ + "Assertion", + "Response 2 does not come from cache" + ], + "other-age-delay": [ + "Assertion", + "Response 1 age header not present." + ], + "other-age-gen": true, + "other-age-update-expires": [ + "Setup", + "Response 1 header Age is \"null\", not \"30\"" + ], + "other-age-update-max-age": [ + "Setup", + "Response 1 header Age is \"null\", not \"30\"" + ], + "other-authorization": true, + "other-authorization-must-revalidate": [ + "Assertion", + "Response 2 does not come from cache" + ], + "other-authorization-public": true, + "other-authorization-smaxage": [ + "Assertion", + "Response 2 does not come from cache" + ], + "other-cookie": true, + "other-date-update": true, + "other-date-update-expires": true, + "other-date-update-expires-update": true, + "other-fresh-content-disposition-attachment": true, + "other-heuristic-content-disposition-attachment": true, + "other-set-cookie": true, + "partial-store-complete-reuse-partial": [ + "Assertion", + "Response 2 status is 200, not 206" + ], + "partial-store-complete-reuse-partial-no-last": [ + "Assertion", + "Response 2 status is 200, not 206" + ], + "partial-store-complete-reuse-partial-suffix": [ + "Assertion", + "Response 2 status is 200, not 206" + ], + "partial-store-partial-complete": [ + "Assertion", + "Request 2 header range is \"undefined\", not \"bytes=5-\"" + ], + "partial-store-partial-reuse-partial": [ + "Assertion", + "Response 2 does not come from cache" + ], + "partial-store-partial-reuse-partial-absent": [ + "Assertion", + "Response 2 does not come from cache" + ], + "partial-store-partial-reuse-partial-byterange": [ + "Assertion", + "Response 2 does not come from cache" + ], + "partial-store-partial-reuse-partial-suffix": [ + "Assertion", + "Response 2 does not come from cache" + ], + "partial-use-headers": [ + "Setup", + "Response 2 status is 200, not 206" + ], + "partial-use-stored-headers": [ + "Setup", + "Response 2 status is 200, not 206" + ], + "pragma-request-extension": true, + "pragma-request-no-cache": true, + "pragma-response-extension": true, + "pragma-response-no-cache": [ + "Assertion", + "Response 2 does not come from cache" + ], + "pragma-response-no-cache-heuristic": [ + "Assertion", + "Response 2 does not come from cache" + ], + "query-args-different": true, + "query-args-same": true, + "stale-503": [ + "Assertion", + "Response 2 does not come from cache" + ], + "stale-close": [ + "TypeError", + "fetch failed" + ], + "stale-close-must-revalidate": [ + "TypeError", + "fetch failed" + ], + "stale-close-no-cache": [ + "TypeError", + "fetch failed" + ], + "stale-close-proxy-revalidate": [ + "TypeError", + "fetch failed" + ], + "stale-close-s-maxage=2": [ + "TypeError", + "fetch failed" + ], + "stale-sie-503": [ + "TypeError", + "fetch failed" + ], + "stale-sie-close": [ + "TypeError", + "fetch failed" + ], + "stale-warning-become": [ + "TypeError", + "fetch failed" + ], + "stale-warning-stored": [ + "TypeError", + "fetch failed" + ], + "stale-while-revalidate": [ + "Assertion", + "Response 2 does not come from cache" + ], + "stale-while-revalidate-window": [ + "Setup", + "Response 2 does not come from cache" + ], + "status-200-fresh": true, + "status-200-must-understand": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-200-stale": true, + "status-203-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-203-stale": true, + "status-204-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-204-stale": true, + "status-299-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-299-stale": true, + "status-301-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-301-stale": true, + "status-302-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-302-stale": true, + "status-303-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-303-stale": true, + "status-307-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-307-stale": true, + "status-308-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-308-stale": true, + "status-400-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-400-stale": true, + "status-404-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-404-stale": true, + "status-410-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-410-stale": true, + "status-499-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-499-stale": true, + "status-500-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-500-stale": true, + "status-502-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-502-stale": true, + "status-503-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-503-stale": true, + "status-504-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-504-stale": true, + "status-599-fresh": [ + "Assertion", + "Response 2 does not come from cache" + ], + "status-599-must-understand": true, + "status-599-stale": true, + "vary-2-match": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-2-match-omit": true, + "vary-2-no-match": true, + "vary-3-match": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-3-no-match": true, + "vary-3-omit": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-3-order": true, + "vary-cache-key": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-invalidate": [ + "Assertion", + "Response 3 does not come from cache" + ], + "vary-match": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-no-match": true, + "vary-normalise-combine": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-normalise-lang-case": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-normalise-lang-order": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-normalise-lang-select": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-normalise-lang-space": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-normalise-space": [ + "Assertion", + "Response 2 does not come from cache" + ], + "vary-omit": true, + "vary-omit-stored": true, + "vary-star": true, + "vary-syntax-empty-star": true, + "vary-syntax-empty-star-lines": true, + "vary-syntax-foo-star": true, + "vary-syntax-star": true, + "vary-syntax-star-foo": true, + "vary-syntax-star-star": true, + "vary-syntax-star-star-lines": true +} diff --git a/test/fixtures/cache-tests/results/index.mjs b/test/fixtures/cache-tests/results/index.mjs index 3d1afde079f..aaa7a525a18 100644 --- a/test/fixtures/cache-tests/results/index.mjs +++ b/test/fixtures/cache-tests/results/index.mjs @@ -61,6 +61,13 @@ export default [ version: '0.7.0', link: 'https://github.com/http-tests/cache-tests/wiki/Caddy' }, + { + file: 'haproxy.json', + name: 'HAProxy', + type: 'rev-proxy', + version: '3.0.8-1ubuntu1', + link: 'https://github.com/http-tests/cache-tests/wiki/HAProxy' + }, { file: 'fastly.json', name: 'Fastly', diff --git a/test/fixtures/cache-tests/results/nginx.json b/test/fixtures/cache-tests/results/nginx.json index 1f12ac9984e..c3dd75bf5a9 100644 --- a/test/fixtures/cache-tests/results/nginx.json +++ b/test/fixtures/cache-tests/results/nginx.json @@ -564,6 +564,22 @@ "Assertion", "Response 2 does not come from cache" ], + "interim-102": [ + "Assertion", + "Response 2 does not come from cache" + ], + "interim-103": [ + "AbortError", + "This operation was aborted" + ], + "interim-no-header-reuse": [ + "Assertion", + "Response 2 does not come from cache" + ], + "interim-not-cached": [ + "Assertion", + "Response 2 does not come from cache" + ], "invalidate-DELETE": [ "Assertion", "Response 3 comes from cache" diff --git a/test/fixtures/cache-tests/results/squid.json b/test/fixtures/cache-tests/results/squid.json index 906f12a9013..1872efc917e 100644 --- a/test/fixtures/cache-tests/results/squid.json +++ b/test/fixtures/cache-tests/results/squid.json @@ -438,6 +438,10 @@ "heuristic-delta-60": true, "heuristic-delta-600": true, "heuristic-delta-86400": true, + "interim-102": true, + "interim-103": true, + "interim-no-header-reuse": true, + "interim-not-cached": true, "invalidate-DELETE": true, "invalidate-DELETE-cl": true, "invalidate-DELETE-failed": true, diff --git a/test/fixtures/cache-tests/results/trafficserver.json b/test/fixtures/cache-tests/results/trafficserver.json index c2112d1ad18..93e49f9f8a5 100644 --- a/test/fixtures/cache-tests/results/trafficserver.json +++ b/test/fixtures/cache-tests/results/trafficserver.json @@ -456,6 +456,22 @@ "Assertion", "Response 2 does not come from cache" ], + "interim-102": [ + "AbortError", + "This operation was aborted" + ], + "interim-103": [ + "Assertion", + "Interim response 1 not received" + ], + "interim-no-header-reuse": [ + "Assertion", + "Interim response 1 not received" + ], + "interim-not-cached": [ + "Assertion", + "Interim response 1 not received" + ], "invalidate-DELETE": [ "Setup", "Response 2 status is 403, not 200" diff --git a/test/fixtures/cache-tests/results/varnish.json b/test/fixtures/cache-tests/results/varnish.json index 067c395fc15..75faf830bfc 100644 --- a/test/fixtures/cache-tests/results/varnish.json +++ b/test/fixtures/cache-tests/results/varnish.json @@ -498,6 +498,22 @@ "Assertion", "Response 2 does not come from cache" ], + "interim-102": [ + "Setup", + "Response 1 status is 503, not 200" + ], + "interim-103": [ + "Setup", + "Response 1 status is 503, not 200" + ], + "interim-no-header-reuse": [ + "Setup", + "Response 1 status is 503, not 200" + ], + "interim-not-cached": [ + "Setup", + "Response 1 status is 503, not 200" + ], "invalidate-DELETE": [ "Assertion", "Response 3 comes from cache" diff --git a/test/fixtures/cache-tests/test-engine/client/test.mjs b/test/fixtures/cache-tests/test-engine/client/test.mjs index 56487a1135f..fb08d807e42 100644 --- a/test/fixtures/cache-tests/test-engine/client/test.mjs +++ b/test/fixtures/cache-tests/test-engine/client/test.mjs @@ -18,7 +18,7 @@ export async function makeTest (test) { const fetchFunctions = [] for (let i = 0; i < requests.length; ++i) { fetchFunctions.push({ - code: idx => { + code: async idx => { const reqConfig = requests[idx] const reqNum = idx + 1 const url = clientUtils.makeTestUrl(uuid, reqConfig) @@ -32,11 +32,20 @@ export async function makeTest (test) { controller.abort() }, config.requestTimeout * 1000) init.signal = controller.signal + + const interimResponses = [] + if ('expected_interim_responses' in reqConfig) { + // Dynamic import since undici is only available in Node.js + const undici = await import('undici') + const dispatcher = new undici.Agent().compose(clientUtils.interimResponsesCollectingInterceptor(interimResponses)) + init.dispatcher = dispatcher + } + if (test.dump === true) clientUtils.logRequest(url, init, reqNum) return fetch(url, init) .then(response => { responses.push(response) - return checkResponse(test, requests, idx, response) + return checkResponse(test, requests, idx, response, interimResponses) }) .finally(() => { clearTimeout(timeout) @@ -84,11 +93,11 @@ export async function makeTest (test) { }) } -function checkResponse (test, requests, idx, response) { +function checkResponse (test, requests, idx, response, interimResponses) { const reqNum = idx + 1 const reqConfig = requests[idx] const resNum = parseInt(response.headers.get('Server-Request-Count')) - if (test.dump === true) clientUtils.logResponse(response, reqNum) + if (test.dump === true) clientUtils.logResponse(response, interimResponses, reqNum) // catch retries if (response.headers.has('Request-Numbers')) { @@ -185,6 +194,37 @@ function checkResponse (test, requests, idx, response) { } }) } + + // check interim responses + if ('expected_interim_responses' in reqConfig) { + const isSetup = setupCheck(reqConfig, 'expected_interim_responses') + + reqConfig.expected_interim_responses.forEach(([statusCode, headers = []], idx) => { + if (interimResponses[idx] == null) { + assert(isSetup, false, `Interim response ${idx + 1} not received`) + } else { + assert(isSetup, interimResponses[idx][0] === statusCode, `Interim response ${idx + 1} status is ${interimResponses[idx][0]}, not ${statusCode}`) + + const receivedHeaders = interimResponses[idx][1] + headers.forEach(([header, value]) => { + if (typeof header === 'string') { + assert(isSetup, header in receivedHeaders, + `Interim response ${idx + 1} ${header} header not present.`) + } else if (header.length > 2) { + assert(isSetup, header in receivedHeaders, + `Interim response ${idx + 1} ${header} header not present.`) + + const receivedValue = receivedHeaders[header] + assert(isSetup, value === receivedValue, + `Interim response ${idx + 1} header ${header} is ${receivedValue}, should ${value}`) + } else { + console.log('ERROR: Unknown header item in expected_interim_responses', header) + } + }) + } + }) + } + return response.text().then(makeCheckResponseBody(test, reqConfig, response.status)) } diff --git a/test/fixtures/cache-tests/test-engine/client/utils.mjs b/test/fixtures/cache-tests/test-engine/client/utils.mjs index 21d918d1e98..a23f3e9fa63 100644 --- a/test/fixtures/cache-tests/test-engine/client/utils.mjs +++ b/test/fixtures/cache-tests/test-engine/client/utils.mjs @@ -72,11 +72,61 @@ export function logRequest (url, init, reqNum) { console.log('') } -export function logResponse (response, reqNum) { +export function logResponse (response, interimResponses, reqNum) { console.log(`${defines.GREEN}=== Client response ${reqNum}${defines.NC}`) + for (const [statusCode, headers] of interimResponses) { + console.log(` HTTP ${statusCode}`) + for (const [key, value] of Object.entries(headers)) { + console.log(` ${key}: ${value}`) + } + console.log('') + } console.log(` HTTP ${response.status} ${response.statusText}`) response.headers.forEach((hvalue, hname) => { // for some reason, node-fetch reverses these console.log(` ${hname}: ${hvalue}`) }) console.log('') } + +class InterimResponsesCollectingHandler { + #handler + #interimResponses + + constructor (handler, interimResponses) { + this.#handler = handler + this.#interimResponses = interimResponses + } + + onRequestStart (controller, context) { + this.#handler.onRequestStart?.(controller, context) + } + + onRequestUpgrade (controller, statusCode, headers, socket) { + this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket) + } + + onResponseStart (controller, statusCode, headers, statusMessage) { + if (statusCode < 200) this.#interimResponses.push([statusCode, headers]) + this.#handler.onResponseStart?.(controller, statusCode, headers, statusMessage) + } + + onResponseData (controller, data) { + this.#handler.onResponseData?.(controller, data) + } + + onResponseEnd (controller, trailers) { + this.#handler.onResponseEnd?.(controller, trailers) + } + + onResponseError (controller, err) { + this.#handler.onResponseError?.(controller, err) + } +} + +export function interimResponsesCollectingInterceptor (collectInto) { + return (dispatch) => { + return (opts, handler) => { + return dispatch(opts, new InterimResponsesCollectingHandler(handler, collectInto)) + } + } +} diff --git a/test/fixtures/cache-tests/test-engine/lib/testsuite-schema.json b/test/fixtures/cache-tests/test-engine/lib/testsuite-schema.json index 95294ff81a6..1890c8da543 100644 --- a/test/fixtures/cache-tests/test-engine/lib/testsuite-schema.json +++ b/test/fixtures/cache-tests/test-engine/lib/testsuite-schema.json @@ -133,6 +133,102 @@ "description": "Whether to rewrite Location and Content-Location to full URLs", "type": "boolean" }, + "interim_responses": { + "description": "Interim responses to send before the final response", + "type": "array", + "items": { + "oneOf": [ + { + "description": "Status code only", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": [ + { + "$ref": "#/definitions/status-code" + } + ] + }, + { + "description": "Status code and headers", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "$ref": "#/definitions/status-code" + }, + { + "description": "Interim response headers", + "type": "array", + "items": { + "type": "array", + "additionalItems": false, + "minItems": 2, + "maxItems": 2, + "items": [ + { + "$ref": "#/definitions/field-name" + }, + { + "$ref": "#/definitions/magic-field-value" + } + ] + } + } + ] + } + ] + } + }, + "expected_interim_responses": { + "description": "Interim responses expected to be received by the client", + "type": "array", + "items": { + "oneOf": [ + { + "description": "Status code only", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": [ + { + "$ref": "#/definitions/status-code" + } + ] + }, + { + "description": "Status code and headers", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "$ref": "#/definitions/status-code" + }, + { + "description": "Expected interim response headers", + "type": "array", + "items": { + "type": "array", + "additionalItems": false, + "minItems": 2, + "maxItems": 2, + "items": [ + { + "$ref": "#/definitions/field-name" + }, + { + "$ref": "#/definitions/magic-field-value" + } + ] + } + } + ] + } + ] + } + }, "magic_ims": { "description": "Whether to rewrite If-Modified-Since to a delta from the previous Last-Modified", "type": "boolean" diff --git a/test/fixtures/cache-tests/test-engine/lib/tpl/checks.liquid b/test/fixtures/cache-tests/test-engine/lib/tpl/checks.liquid index 730290f5d81..0193a06367d 100644 --- a/test/fixtures/cache-tests/test-engine/lib/tpl/checks.liquid +++ b/test/fixtures/cache-tests/test-engine/lib/tpl/checks.liquid @@ -7,6 +7,13 @@ ### The following checks will be performed: +{%- if request.interim_responses -%} +{%- for interim_response in request.interim_responses %} +- The client will check that an interim response with the `{{ interim_response[0] }}` status code{% if interim_response[1] %} and the following headers{% endif %} is received. +{%- render 'header-list' with interim_response[1] as headers %} +{%- endfor -%} +{%- endif -%} + {%- if request.expected_type %} - The client will check that this response {% case request.expected_type %}{% when "cached" %}is cached{% when "not_cached" %}is not cached{% when "lm_validated" %}is validated using `Last-Modified`{% when "etag_validated" %}is validated using `ETag`{% endcase %} {% if test.setup_tests contains "expected_type" %}{{ setup_prop }}{% endif %}{% endif -%} diff --git a/test/fixtures/cache-tests/test-engine/lib/tpl/explain-test.liquid b/test/fixtures/cache-tests/test-engine/lib/tpl/explain-test.liquid index 975a6ceedfa..c791ab023c6 100644 --- a/test/fixtures/cache-tests/test-engine/lib/tpl/explain-test.liquid +++ b/test/fixtures/cache-tests/test-engine/lib/tpl/explain-test.liquid @@ -62,8 +62,16 @@ The server will pause for {{ request.response_pause }} seconds before responding {%- if request.response_status or request.response_headers or request.response_body %} -### The server sends a response containing: +### The server sends {% if request.interim_responses %}responses{% else %}a response{% endif %} containing: + ~~~ +{% if request.interim_responses -%} +{% for interim_response in request.interim_responses -%} +HTTP/1.1 {{ interim_response[0] }} +{% for header in interim_response[1] %}{% render 'header-magic' with header as header %} +{% endfor %} +{% endfor -%} +{%- endif -%} {% if request.expected_type == "lm_validated" or request.expected_type = "etag_validated" -%} HTTP/1.1 304 Not Modified {%- else -%} diff --git a/test/fixtures/cache-tests/test-engine/server/handle-test.mjs b/test/fixtures/cache-tests/test-engine/server/handle-test.mjs index 13aae63bee6..808020469eb 100644 --- a/test/fixtures/cache-tests/test-engine/server/handle-test.mjs +++ b/test/fixtures/cache-tests/test-engine/server/handle-test.mjs @@ -43,6 +43,17 @@ function continueHandleTest (uuid, request, response, requests, serverState) { const previousConfig = requests[reqNum - 2] const now = Date.now() + const interimResponses = reqConfig.interim_responses || [] + for (const [status, headers = []] of interimResponses) { + if (status === 102) { + response.writeProcessing() + } else if (status === 103) { + response.writeEarlyHints(Object.fromEntries(headers)) + } else { + console.log(`ERROR: Sending ${status} is not yet supported`) + } + } + // Determine what the response status should be let httpStatus = reqConfig.response_status || [200, 'OK'] if ('expected_type' in reqConfig && reqConfig.expected_type.endsWith('validated')) { @@ -114,5 +125,5 @@ function continueHandleTest (uuid, request, response, requests, serverState) { } // logging - if (reqConfig.dump) logResponse(response, srvReqNum) + if (reqConfig.dump) logResponse(response, interimResponses, srvReqNum) } diff --git a/test/fixtures/cache-tests/test-engine/server/utils.mjs b/test/fixtures/cache-tests/test-engine/server/utils.mjs index 7f37a665313..ce496a86116 100644 --- a/test/fixtures/cache-tests/test-engine/server/utils.mjs +++ b/test/fixtures/cache-tests/test-engine/server/utils.mjs @@ -40,11 +40,18 @@ export function logRequest (request, reqNum) { console.log('') } -export function logResponse (response, resNum) { +export function logResponse (response, interimResponses, resNum) { console.log(`${BLUE}=== Server response ${resNum}${NC}`) if (response === 'disconnect') { console.log(' [ server disconnect ]') } else { + for (const [statusCode, headers] of interimResponses) { + console.log(` HTTP ${statusCode}`) + for (const [key, value] of headers) { + console.log(` ${key}: ${value}`) + } + console.log('') + } console.log(` HTTP ${response.statusCode} ${response.statusPhrase}`) for (const [key, value] of Object.entries(response.getHeaders())) { console.log(` ${key}: ${value}`) diff --git a/test/fixtures/cache-tests/tests/index.mjs b/test/fixtures/cache-tests/tests/index.mjs index 3b7378c1537..1a79eff426f 100644 --- a/test/fixtures/cache-tests/tests/index.mjs +++ b/test/fixtures/cache-tests/tests/index.mjs @@ -22,5 +22,6 @@ import partial from './partial.mjs' import auth from './authorization.mjs' import other from './other.mjs' import cdncc from './cdn-cache-control.mjs' +import interim from './interim.mjs' -export default [ccFreshness, ccParse, ageParse, expires, expiresParse, ccResponse, stale, heuristic, methods, statuses, ccRequest, pragma, vary, varyParse, conditionalLm, conditionalEtag, headers, update304, updateHead, invalidation, partial, auth, other, cdncc] +export default [ccFreshness, ccParse, ageParse, expires, expiresParse, ccResponse, stale, heuristic, methods, statuses, ccRequest, pragma, vary, varyParse, conditionalLm, conditionalEtag, headers, update304, updateHead, invalidation, partial, auth, other, cdncc, interim] diff --git a/test/fixtures/cache-tests/tests/interim.mjs b/test/fixtures/cache-tests/tests/interim.mjs new file mode 100644 index 00000000000..4ffb3139dd2 --- /dev/null +++ b/test/fixtures/cache-tests/tests/interim.mjs @@ -0,0 +1,124 @@ +export default + +{ + name: 'Interim Response Handling', + id: 'interim', + description: 'These tests check how caches handle interim responses.', + tests: [ + { + name: 'An optimal HTTP cache passes a 102 response through and caches the final response', + id: 'interim-102', + browser_skip: true, // Fetch API in browsers don't expose interim responses + kind: 'optimal', + requests: [ + { + interim_responses: [[102]], + expected_interim_responses: [[102]], + response_headers: [ + ['Cache-Control', 'max-age=100000'], + ['Date', 0] + ], + pause_after: true + }, + { + expected_type: 'cached' + } + ] + }, + { + name: 'An optimal HTTP cache passes a 103 response through and caches the final response', + id: 'interim-103', + browser_skip: true, + kind: 'optimal', + requests: [ + { + interim_responses: [ + [103, [ + ['link', '; rel=preload; as=style'], + ['x-my-header', 'test'] + ]] + ], + expected_interim_responses: [ + [103, [ + ['link', '; rel=preload; as=style'], + ['x-my-header', 'test'] + ]] + ], + response_headers: [ + ['Cache-Control', 'max-age=100000'], + ['Date', 0] + ], + pause_after: true + }, + { + expected_type: 'cached' + } + ] + }, + { + name: 'An HTTP cache should not cache non-final responses', + id: 'interim-not-cached', + browser_skip: true, + kind: 'required', + requests: [ + { + interim_responses: [ + [103, [ + ['link', '; rel=preload; as=style'] + ]] + ], + expected_interim_responses: [ + [103, [ + ['link', '; rel=preload; as=style'] + ]] + ], + response_headers: [ + ['Cache-Control', 'max-age=100000'], + ['Date', 0] + ], + pause_after: true + }, + { + expected_type: 'cached', + expected_interim_responses: [] + } + ] + }, + { + name: 'An optimal HTTP cache should not store headers from non-final responses', + id: 'interim-no-header-reuse', + browser_skip: true, + kind: 'optimal', + requests: [ + { + interim_responses: [ + [103, [ + ['link', '; rel=preload; as=style'], + ['x-my-header', 'test'] + ]] + ], + expected_interim_responses: [ + [103, [ + ['link', '; rel=preload; as=style'], + ['x-my-header', 'test'] + ]] + ], + response_headers: [ + ['Cache-Control', 'max-age=100000'], + ['Date', 0] + ], + expected_response_headers_missing: [ + 'x-my-header' + ], + pause_after: true + }, + { + expected_type: 'cached', + expected_response_headers_missing: [ + 'x-my-header' + ] + } + ] + } + ] +}