From 7be59c8a49c037b5753003210149be4fc6441519 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 23 Mar 2026 13:40:30 -0700 Subject: [PATCH 1/2] Add 204 and 308 to heuristically cacheable status codes RFC 9110 Section 15.1 defines 204 (No Content) and 308 (Permanent Redirect) as heuristically cacheable by default, but they were missing from the allowlist in is_response_cacheable(). This meant these responses would only be cached via negative caching configuration rather than being treated as cacheable by default like their counterparts (200 and 301). --- src/proxy/http/HttpTransact.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc index 46562e5ed78..c54cbf11b35 100644 --- a/src/proxy/http/HttpTransact.cc +++ b/src/proxy/http/HttpTransact.cc @@ -6564,7 +6564,8 @@ HttpTransact::is_response_cacheable(State *s, HTTPHdr *request, HTTPHdr *respons } if ((response_code == HTTPStatus::OK) || (response_code == HTTPStatus::NOT_MODIFIED) || - (response_code == HTTPStatus::NON_AUTHORITATIVE_INFORMATION) || (response_code == HTTPStatus::MOVED_PERMANENTLY) || + (response_code == HTTPStatus::NON_AUTHORITATIVE_INFORMATION) || (response_code == HTTPStatus::NO_CONTENT) || + (response_code == HTTPStatus::MOVED_PERMANENTLY) || (response_code == HTTPStatus::PERMANENT_REDIRECT) || (response_code == HTTPStatus::MULTIPLE_CHOICES) || (response_code == HTTPStatus::GONE)) { TxnDbg(dbg_ctl_http_trans, "YES response code seems fine"); return true; From ed5868d6d6f94a68a331ef986f812f7ed564da8b Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 23 Mar 2026 14:46:25 -0700 Subject: [PATCH 2/2] Add autest for heuristic caching of status codes per RFC 9110 Verifies that responses with heuristically cacheable status codes (200, 203, 204, 300, 301, 308, 410) are cached when only Last-Modified is present (no Cache-Control or Expires), and that non-cacheable codes (302, 307, 400, 403) are not cached. Negative caching is disabled to isolate the heuristic path. --- .../cache/cache-heuristic-status.test.py | 28 + .../replay/cache-heuristic-status.replay.yaml | 507 ++++++++++++++++++ 2 files changed, 535 insertions(+) create mode 100644 tests/gold_tests/cache/cache-heuristic-status.test.py create mode 100644 tests/gold_tests/cache/replay/cache-heuristic-status.replay.yaml diff --git a/tests/gold_tests/cache/cache-heuristic-status.test.py b/tests/gold_tests/cache/cache-heuristic-status.test.py new file mode 100644 index 00000000000..e191be26345 --- /dev/null +++ b/tests/gold_tests/cache/cache-heuristic-status.test.py @@ -0,0 +1,28 @@ +''' +Test heuristic caching of status codes per RFC 9110 Section 15.1. +''' +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Test.Summary = ''' +Test heuristic caching of status codes per RFC 9110 Section 15.1. + +Verifies that responses with heuristically cacheable status codes (200, 203, +204, 300, 301, 308, 410) are cached when only Last-Modified is present, and +that non-cacheable codes (302, 307, 400, 403) are not. +''' + +Test.ATSReplayTest(replay_file="replay/cache-heuristic-status.replay.yaml") diff --git a/tests/gold_tests/cache/replay/cache-heuristic-status.replay.yaml b/tests/gold_tests/cache/replay/cache-heuristic-status.replay.yaml new file mode 100644 index 00000000000..fd6e9b204b9 --- /dev/null +++ b/tests/gold_tests/cache/replay/cache-heuristic-status.replay.yaml @@ -0,0 +1,507 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Verify heuristic caching behavior for various HTTP status codes per +# RFC 9110 Section 15.1. Responses contain only a Last-Modified header +# (no Cache-Control or Expires) so cacheability depends entirely on the +# status code allowlist in HttpTransact::is_response_cacheable(). +# +# Negative caching is disabled to isolate the heuristic path. +# + +meta: + version: "1.0" + +autest: + description: 'Verify heuristic caching by status code per RFC 9110 Section 15.1' + + dns: + name: 'dns-heuristic-status' + + server: + name: 'server-heuristic-status' + + client: + name: 'client-heuristic-status' + + ats: + name: 'ts-heuristic-status' + process_config: + enable_cache: true + + records_config: + proxy.config.diags.debug.enabled: 1 + proxy.config.diags.debug.tags: 'http' + proxy.config.http.insert_age_in_response: 0 + proxy.config.http.negative_caching_enabled: 0 + proxy.config.http.cache.required_headers: 0 + + remap_config: + - from: "http://example.com/" + to: "http://backend.example.com:{SERVER_HTTP_PORT}/" + + blocks: + - canary_response: &canary_response + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 16 ] + - [ Cache-Control, max-age=300 ] + +sessions: +- transactions: + + # ===================================================================== + # Heuristically cacheable status codes. + # + # Each test sends a request, gets a response with only Last-Modified + # (no CC/Expires), then repeats the request. The second request's + # server-response is a canary 200; if we get the original status back, + # the response was served from cache. + # ===================================================================== + + # --- 200 OK --- + - all: { headers: { fields: [[ uuid, 1 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/200 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + + proxy-response: + status: 200 + + - all: { headers: { fields: [[ uuid, 2 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/200 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 200 + + # --- 203 Non-Authoritative Information --- + - all: { headers: { fields: [[ uuid, 3 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/203 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 203 + reason: "Non-Authoritative Information" + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + + proxy-response: + status: 203 + + - all: { headers: { fields: [[ uuid, 4 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/203 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 203 + + # --- 204 No Content --- + - all: { headers: { fields: [[ uuid, 5 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/204 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 204 + reason: "No Content" + headers: + fields: + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + + proxy-response: + status: 204 + + - all: { headers: { fields: [[ uuid, 6 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/204 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 204 + + # --- 300 Multiple Choices --- + - all: { headers: { fields: [[ uuid, 7 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/300 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 300 + reason: "Multiple Choices" + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + - [ Location, "http://example.com/choice1" ] + + proxy-response: + status: 300 + + - all: { headers: { fields: [[ uuid, 8 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/300 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 300 + + # --- 301 Moved Permanently --- + - all: { headers: { fields: [[ uuid, 9 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/301 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 301 + reason: "Moved Permanently" + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + - [ Location, "http://example.com/new-location" ] + + proxy-response: + status: 301 + + - all: { headers: { fields: [[ uuid, 10 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/301 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 301 + + # --- 308 Permanent Redirect --- + - all: { headers: { fields: [[ uuid, 11 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/308 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 308 + reason: "Permanent Redirect" + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + - [ Location, "http://example.com/permanent" ] + + proxy-response: + status: 308 + + - all: { headers: { fields: [[ uuid, 12 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/308 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 308 + + # --- 410 Gone --- + - all: { headers: { fields: [[ uuid, 13 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/410 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 410 + reason: "Gone" + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + + proxy-response: + status: 410 + + - all: { headers: { fields: [[ uuid, 14 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /heuristic/410 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 410 + + # ===================================================================== + # Non-cacheable status codes without explicit cache directives. + # + # These are NOT in the heuristic allowlist and negative caching is + # disabled, so they should not be cached. The second request should + # go to the origin and return the canary 200. + # ===================================================================== + + # --- 302 Found (explicitly rejected) --- + - all: { headers: { fields: [[ uuid, 15 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /not-cacheable/302 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 302 + reason: "Found" + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + - [ Location, "http://example.com/temporary" ] + + proxy-response: + status: 302 + + - all: { headers: { fields: [[ uuid, 16 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /not-cacheable/302 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 200 + + # --- 307 Temporary Redirect (explicitly rejected) --- + - all: { headers: { fields: [[ uuid, 17 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /not-cacheable/307 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 307 + reason: "Temporary Redirect" + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + - [ Location, "http://example.com/temporary" ] + + proxy-response: + status: 307 + + - all: { headers: { fields: [[ uuid, 18 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /not-cacheable/307 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 200 + + # --- 400 Bad Request --- + - all: { headers: { fields: [[ uuid, 19 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /not-cacheable/400 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 400 + reason: "Bad Request" + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + + proxy-response: + status: 400 + + - all: { headers: { fields: [[ uuid, 20 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /not-cacheable/400 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 200 + + # --- 403 Forbidden --- + - all: { headers: { fields: [[ uuid, 21 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /not-cacheable/403 + headers: + fields: + - [ Host, example.com ] + + server-response: + status: 403 + reason: "Forbidden" + headers: + fields: + - [ Content-Length, 16 ] + - [ Last-Modified, "Mon, 16 Mar 2026 00:00:00 GMT" ] + + proxy-response: + status: 403 + + - all: { headers: { fields: [[ uuid, 22 ]]}} + client-request: + method: "GET" + version: "1.1" + scheme: "http" + url: /not-cacheable/403 + headers: + fields: + - [ Host, example.com ] + delay: 100ms + + <<: *canary_response + + proxy-response: + status: 200