Skip to content

Commit c75c797

Browse files
authored
Do not invalidate cached resources upon error responses to unsafe methods (#7864)
Before this change, cached resources would be deleted when the server replied to a request with an unsafe method (POST, PUT, etc.) with an error response code. This patch changes that behavior so that it is only deleted when the response has a successful response code. This seeks to comply more narrowly with the wording of RFC 7234, section-4.4: A cache MUST invalidate the effective request URI (Section 5.5 of [RFC7230]) when it receives a non-error response to a request with a method whose safety is unknown. Note that we were technically in compliance with this before since we always invalidated, regardless of the error response code. This, however, was too broad in that we invalidated when we didn't need to. Now we will only invalidate when the response code indicates a successful response to the request with an unsafe method. This also fixes our feature that caches responses to POST requests.
1 parent 0a40591 commit c75c797

File tree

8 files changed

+904
-7
lines changed

8 files changed

+904
-7
lines changed

proxy/hdrs/HTTP.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ class HTTPHdr : public MIMEHdr
508508
void version_set(HTTPVersion version);
509509

510510
const char *method_get(int *length);
511-
int method_get_wksidx();
511+
int method_get_wksidx() const;
512512
void method_set(const char *value, int length);
513513

514514
URL *url_create(URL *url);
@@ -957,7 +957,7 @@ HTTPHdr::method_get(int *length)
957957
}
958958

959959
inline int
960-
HTTPHdr::method_get_wksidx()
960+
HTTPHdr::method_get_wksidx() const
961961
{
962962
ink_assert(valid());
963963
ink_assert(m_http->m_polarity == HTTP_TYPE_REQUEST);

proxy/http/HttpTransact.cc

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,6 +2085,14 @@ HttpTransact::OSDNSLookup(State *s)
20852085
} else if (s->cache_lookup_result == CACHE_LOOKUP_MISS || s->cache_info.action == CACHE_DO_NO_ACTION) {
20862086
TRANSACT_RETURN(SM_ACTION_API_OS_DNS, HandleCacheOpenReadMiss);
20872087
// DNS lookup is done if the lookup failed and need to call Handle Cache Open Read Miss
2088+
} else if (s->cache_info.action == CACHE_PREPARE_TO_WRITE && s->http_config_param->cache_post_method == 1 &&
2089+
s->method == HTTP_WKSIDX_POST) {
2090+
// By virtue of being here, we are intending to forward the request on
2091+
// to the server. If we marked this as CACHE_PREPARE_TO_WRITE and this
2092+
// is a POST request whose response we intend to write, then we have to
2093+
// proceed from here by calling the function that handles this as a
2094+
// miss.
2095+
TRANSACT_RETURN(SM_ACTION_API_OS_DNS, HandleCacheOpenReadMiss);
20882096
} else {
20892097
build_error_response(s, HTTP_STATUS_INTERNAL_SERVER_ERROR, "Invalid Cache Lookup result", "default");
20902098
Log::error("HTTP: Invalid CACHE LOOKUP RESULT : %d", s->cache_lookup_result);
@@ -3093,7 +3101,8 @@ HttpTransact::build_response_from_cache(State *s, HTTPWarningCode warning_code)
30933101
// fall through
30943102
default:
30953103
SET_VIA_STRING(VIA_DETAIL_CACHE_LOOKUP, VIA_DETAIL_HIT_SERVED);
3096-
if (s->method == HTTP_WKSIDX_GET || s->api_resp_cacheable == true) {
3104+
if (s->method == HTTP_WKSIDX_GET || (s->http_config_param->cache_post_method == 1 && s->method == HTTP_WKSIDX_POST) ||
3105+
s->api_resp_cacheable == true) {
30973106
// send back the full document to the client.
30983107
TxnDebug("http_trans", "[build_response_from_cache] Match! Serving full document.");
30993108
s->cache_info.action = CACHE_DO_SERVE;
@@ -3323,7 +3332,7 @@ HttpTransact::HandleCacheOpenReadMiss(State *s)
33233332
if (GET_VIA_STRING(VIA_DETAIL_CACHE_LOOKUP) == ' ') {
33243333
SET_VIA_STRING(VIA_DETAIL_CACHE_LOOKUP, VIA_DETAIL_MISS_NOT_CACHED);
33253334
}
3326-
// We do a cache lookup for DELETE and PUT requests as well.
3335+
// We do a cache lookup for some non-GET requests as well.
33273336
// We must, however, not cache the responses to these requests.
33283337
if (does_method_require_cache_copy_deletion(s->http_config_param, s->method) && s->api_req_cacheable == false) {
33293338
s->cache_info.action = CACHE_DO_NO_ACTION;
@@ -4475,10 +4484,11 @@ HttpTransact::handle_cache_operation_on_forward_server_response(State *s)
44754484
// cacheability of server response, and request method
44764485
// precondition: s->cache_info.action is one of the following
44774486
// CACHE_DO_UPDATE, CACHE_DO_WRITE, or CACHE_DO_DELETE
4487+
int const server_request_method = s->hdr_info.server_request.method_get_wksidx();
44784488
if (s->api_server_response_no_store) {
44794489
s->cache_info.action = CACHE_DO_NO_ACTION;
44804490
} else if (s->api_server_response_ignore && server_response_code == HTTP_STATUS_OK &&
4481-
s->hdr_info.server_request.method_get_wksidx() == HTTP_WKSIDX_HEAD) {
4491+
server_request_method == HTTP_WKSIDX_HEAD) {
44824492
s->api_server_response_ignore = false;
44834493
ink_assert(s->cache_info.object_read);
44844494
base_response = s->cache_info.object_read->response_get();
@@ -4495,7 +4505,21 @@ HttpTransact::handle_cache_operation_on_forward_server_response(State *s)
44954505
} else if (s->www_auth_content == CACHE_AUTH_STALE && server_response_code == HTTP_STATUS_UNAUTHORIZED) {
44964506
s->cache_info.action = CACHE_DO_NO_ACTION;
44974507
} else if (!cacheable) {
4498-
s->cache_info.action = CACHE_DO_DELETE;
4508+
if (HttpTransactHeaders::is_status_an_error_response(server_response_code) &&
4509+
!HttpTransactHeaders::is_method_safe(server_request_method)) {
4510+
// Only delete the cache entry if the response is successful. For
4511+
// unsuccessful responses, the transaction doesn't invalidate our
4512+
// entry. This behavior complies with RFC 7234, section 4.4 which
4513+
// stipulates that the entry only need be invalidated for non-error
4514+
// responses:
4515+
//
4516+
// A cache MUST invalidate the effective request URI (Section 5.5 of
4517+
// [RFC7230]) when it receives a non-error response to a request
4518+
// with a method whose safety is unknown.
4519+
s->cache_info.action = CACHE_DO_NO_ACTION;
4520+
} else {
4521+
s->cache_info.action = CACHE_DO_DELETE;
4522+
}
44994523
} else if (s->method == HTTP_WKSIDX_HEAD) {
45004524
s->cache_info.action = CACHE_DO_DELETE;
45014525
} else {
@@ -4513,7 +4537,19 @@ HttpTransact::handle_cache_operation_on_forward_server_response(State *s)
45134537
}
45144538

45154539
} else if (s->cache_info.action == CACHE_DO_DELETE) {
4516-
// do nothing
4540+
if (!cacheable && HttpTransactHeaders::is_status_an_error_response(server_response_code) &&
4541+
!HttpTransactHeaders::is_method_safe(server_request_method)) {
4542+
// Only delete the cache entry if the response is successful. For
4543+
// unsuccessful responses, the transaction doesn't invalidate our
4544+
// entry. This behavior complies with RFC 7234, section 4.4 which
4545+
// stipulates that the entry only need be invalidated for non-error
4546+
// responses:
4547+
//
4548+
// A cache MUST invalidate the effective request URI (Section 5.5 of
4549+
// [RFC7230]) when it receives a non-error response to a request
4550+
// with a method whose safety is unknown.
4551+
s->cache_info.action = CACHE_DO_NO_ACTION;
4552+
}
45174553

45184554
} else {
45194555
ink_assert(!("cache action inconsistent with current state"));
@@ -5920,6 +5956,18 @@ HttpTransact::is_cache_response_returnable(State *s)
59205956
SET_VIA_STRING(VIA_DETAIL_CACHE_LOOKUP, VIA_DETAIL_MISS_METHOD);
59215957
return false;
59225958
}
5959+
// We may be caching responses to methods other than GET, such as POST. Make
5960+
// sure that our cached resource has a method that matches the incoming
5961+
// requests's method. If not, then we cannot reply with the cached resource.
5962+
// That is, we cannot reply to an incoming GET request with a response to a
5963+
// previous POST request.
5964+
int const client_request_method = s->hdr_info.client_request.method_get_wksidx();
5965+
int const cached_request_method = s->cache_info.object_read->request_get()->method_get_wksidx();
5966+
if (client_request_method != cached_request_method) {
5967+
SET_VIA_STRING(VIA_CACHE_RESULT, VIA_IN_CACHE_NOT_ACCEPTABLE);
5968+
SET_VIA_STRING(VIA_DETAIL_CACHE_LOOKUP, VIA_DETAIL_MISS_METHOD);
5969+
return false;
5970+
}
59235971
// If cookies in response and no TTL set, we do not cache the doc
59245972
if ((s->cache_control.ttl_in_cache <= 0) &&
59255973
do_cookies_prevent_caching(static_cast<int>(s->txn_conf->cache_responses_to_cookies), &s->hdr_info.client_request,

proxy/http/HttpTransactHeaders.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,17 @@ HttpTransactHeaders::is_this_method_supported(int the_scheme, int the_method)
8787
bool
8888
HttpTransactHeaders::is_method_safe(int method)
8989
{
90+
// See RFC 7231, section 4.2.1.
9091
return (method == HTTP_WKSIDX_GET || method == HTTP_WKSIDX_OPTIONS || method == HTTP_WKSIDX_HEAD || method == HTTP_WKSIDX_TRACE);
9192
}
9293

94+
bool
95+
HttpTransactHeaders::is_status_an_error_response(HTTPStatus response_code)
96+
{
97+
auto const comparable_response_code = static_cast<unsigned int>(response_code);
98+
return (comparable_response_code >= 400) && (comparable_response_code <= 599);
99+
}
100+
93101
bool
94102
HttpTransactHeaders::is_method_idempotent(int method)
95103
{

proxy/http/HttpTransactHeaders.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class HttpTransactHeaders
5656
static bool downgrade_request(bool *origin_server_keep_alive, HTTPHdr *outgoing_request);
5757
static bool is_method_safe(int method);
5858
static bool is_method_idempotent(int method);
59+
static bool is_status_an_error_response(HTTPStatus response_code);
5960

6061
static void generate_and_set_squid_codes(HTTPHdr *header, char *via_string, HttpTransact::SquidLogInfo *squid_codes);
6162

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'''
2+
Verify correct caching behavior with respect to request method.
3+
'''
4+
# Licensed to the Apache Software Foundation (ASF) under one
5+
# or more contributor license agreements. See the NOTICE file
6+
# distributed with this work for additional information
7+
# regarding copyright ownership. The ASF licenses this file
8+
# to you under the Apache License, Version 2.0 (the
9+
# "License"); you may not use this file except in compliance
10+
# with the License. You may obtain a copy of the License at
11+
#
12+
# http://www.apache.org/licenses/LICENSE-2.0
13+
#
14+
# Unless required by applicable law or agreed to in writing, software
15+
# distributed under the License is distributed on an "AS IS" BASIS,
16+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
# See the License for the specific language governing permissions and
18+
# limitations under the License.
19+
20+
Test.Summary = '''
21+
Verify correct caching behavior with respect to request method.
22+
'''
23+
24+
# Test 0: Verify correct POST response handling when caching POST responses is
25+
# disabled.
26+
ts = Test.MakeATSProcess("ts")
27+
replay_file = "replay/post_with_post_caching_disabled.replay.yaml"
28+
server = Test.MakeVerifierServerProcess("server0", replay_file)
29+
ts.Disk.records_config.update({
30+
'proxy.config.diags.debug.enabled': 1,
31+
'proxy.config.diags.debug.tags': 'http.*|cache.*',
32+
'proxy.config.http.insert_age_in_response': 0,
33+
34+
# Caching of POST responses is disabled by default. Verify default behavior
35+
# by leaving it unconfigured.
36+
# 'proxy.config.http.cache.post_method': 0,
37+
})
38+
ts.Disk.remap_config.AddLine(
39+
'map / http://127.0.0.1:{0}'.format(server.Variables.http_port)
40+
)
41+
tr = Test.AddTestRun("Verify correct with POST response caching disabled.")
42+
tr.Processes.Default.StartBefore(server)
43+
tr.Processes.Default.StartBefore(ts)
44+
tr.AddVerifierClientProcess("client0", replay_file, http_ports=[ts.Variables.port])
45+
46+
# Test 1: Verify correct POST response handling when caching POST responses is
47+
# enabled.
48+
ts = Test.MakeATSProcess("ts-cache-post")
49+
replay_file = "replay/post_with_post_caching_enabled.replay.yaml"
50+
server = Test.MakeVerifierServerProcess("server1", replay_file)
51+
ts.Disk.records_config.update({
52+
'proxy.config.diags.debug.enabled': 1,
53+
'proxy.config.diags.debug.tags': 'http.*|cache.*',
54+
'proxy.config.http.insert_age_in_response': 0,
55+
'proxy.config.http.cache.post_method': 1,
56+
})
57+
ts.Disk.remap_config.AddLine(
58+
'map / http://127.0.0.1:{0}'.format(server.Variables.http_port)
59+
)
60+
tr = Test.AddTestRun("Verify correct with POST response caching enabled.")
61+
tr.Processes.Default.StartBefore(server)
62+
tr.Processes.Default.StartBefore(ts)
63+
tr.AddVerifierClientProcess("client1", replay_file, http_ports=[ts.Variables.port])

0 commit comments

Comments
 (0)