Summary
In ATS 9.2.x, when a transaction is routed through HttpTransact::handle_no_cache_operation_on_forward_server_response() (i.e. cache_info.action == CACHE_DO_NO_ACTION, which is the typical path for Range requests when proxy.config.http.cache.range.write = 0, for non-cacheable methods, when caching is disabled, etc.), the via_string slots VIA_SERVER_RESULT and VIA_PROXY_RESULT are only updated for 200 OK and 304 Not Modified. For 206 Partial Content and for everything that falls into the default branch (404, 5xx, …), the function does not call SET_VIA_STRING(...) at all.
As a result, the response Via header for these very common cases looks like:
Via: http/1.1 cache-foo (ATS/9.2.x [cMs f ])
with two empty slots where VIA_SERVER_RESULT and VIA_CACHE_FILL_ACTION should have been recorded. This makes the Via header much less useful for operators trying to distinguish between "origin actually answered" vs. "internally synthesized error" (e.g. DNS failure → 500 Cannot find server.) — both look identical at proxy.config.http.insert_response_via_str = 2.
Affected version
9.2.x (verified against the current 9.2.x branch on GitHub, file proxy/http/HttpTransact.cc)
- Likely present in earlier versions as well; please confirm against
master / 10.0.x.
Reproduction
ATS configuration (defaults are sufficient — these are the relevant ones):
CONFIG proxy.config.http.insert_response_via_str INT 2
CONFIG proxy.config.http.cache.range.lookup INT 1
CONFIG proxy.config.http.cache.range.write INT 0
A simple remap to any HTTP origin, then:
Case A — Range request returning 206
curl -v -x 127.0.0.1:8686 http://www.example.com/videos/output.mp4 -r 0-1 -o /dev/null
Response (excerpt):
HTTP/1.1 206 Partial Content
...
Via: http/1.1 CDN (Up [cMs f ])
Case B — Range request returning 404
curl -v -x 127.0.0.1:8686 http://www.example.com/videos/output.mp4 -r 0-1 -o /dev/null
HTTP/1.1 404 Not Found
...
Via: http/1.1 CDN (Up [cMs f ])
Case C — DNS failure, ATS-synthesized 500
curl -v -x 127.0.0.1:8686 http://www.example.com/
HTTP/1.1 500 Cannot find server.
...
Via: http/1.1 CDN (Up [cMs f ])
In all three cases the Via debug field is identical ([cMs f ]), even though Case A and Case B did contact the origin successfully and Case C never even attempted a server connection.
Expected behavior
For Case A (206) and Case B (404, going through the default branch), the origin did serve the response, so the Via debug field should reflect that:
VIA_SERVER_RESULT = 'S' (VIA_SERVER_SERVED)
VIA_PROXY_RESULT = 'S' (VIA_PROXY_SERVED)
i.e. the field should look like [cMsSf ] — consistent with what already happens for 200/304.
For Case C (DNS failure), the current [cMs f ] is "expected" because no server connection was ever made, but the VIA_ERROR_TYPE slot does carry useful info ('D' for VIA_ERROR_DNS_FAILURE) — it is just not visible at insert_response_via_str = 2 because that level only emits the [VIA_CACHE, VIA_PROXY) window. That part is by design and not the subject of this issue, but it is worth mentioning that operators currently cannot tell Case A/B from Case C without bumping verbosity to 3.
Root cause (source pointers, 9.2.x)
The construction of the response Via debug window happens in HttpTransactHeaders::insert_via_header_in_response():
// proxy/http/HttpTransactHeaders.cc, ~line 872
if (s->txn_conf->insert_response_via_string > 1) {
*via_string++ = '[';
if (s->txn_conf->insert_response_via_string > 2) { // Highest verbosity
via_string += nstrcpy(via_string, incoming_via);
} else {
memcpy(via_string, incoming_via + VIA_CACHE, VIA_PROXY - VIA_CACHE);
via_string += VIA_PROXY - VIA_CACHE;
}
*via_string++ = ']';
...
}
So at verbosity 2 the visible window covers exactly:
| index |
name |
| 2 |
VIA_CACHE |
| 3 |
VIA_CACHE_RESULT |
| 4 |
VIA_SERVER |
| 5 |
VIA_SERVER_RESULT |
| 6 |
VIA_CACHE_FILL |
| 7 |
VIA_CACHE_FILL_ACTION |
via_string[] is initialized in HttpTransact::State() (proxy/http/HttpTransact.h, ~line 830): all slots are filled with ' ', then the six "marker" slots are overwritten with their fixed letters (u/c/s/f/p/e). Result slots are only set later when the corresponding SET_VIA_STRING(...) is executed.
For Range requests with cache_range_write = 0, HandleCacheOpenReadMiss() sets VIA_CACHE_RESULT = 'M' and forces cache_info.action = CACHE_DO_NO_ACTION:
// proxy/http/HttpTransact.cc, ~line 3319
SET_VIA_STRING(VIA_CACHE_RESULT, VIA_CACHE_MISS);
...
} else if ((s->hdr_info.client_request.presence(MIME_PRESENCE_RANGE) && !s->txn_conf->cache_range_write) ||
does_method_effect_cache(s->method) == false || s->range_setup == RANGE_NOT_SATISFIABLE ||
s->range_setup == RANGE_NOT_HANDLED) {
s->cache_info.action = CACHE_DO_NO_ACTION;
}
Because of CACHE_DO_NO_ACTION, handle_response_from_server() then dispatches to handle_no_cache_operation_on_forward_server_response():
// proxy/http/HttpTransact.cc, ~line 4170
// Just tunnel?
TxnDebug("http_trans", "[hfsco] cache action: %s",
HttpDebugNames::get_cache_action_name(s->cache_info.action));
handle_no_cache_operation_on_forward_server_response(s);
And here is the actual problem — case HTTP_STATUS_PARTIAL_CONTENT and default simply don't update the Via slots:
// proxy/http/HttpTransact.cc, ~line 4791
switch (s->hdr_info.server_response.status_get()) {
case HTTP_STATUS_OK:
...
SET_VIA_STRING(VIA_SERVER_RESULT, VIA_SERVER_SERVED);
SET_VIA_STRING(VIA_PROXY_RESULT, VIA_PROXY_SERVED);
...
break;
case HTTP_STATUS_NOT_MODIFIED:
...
SET_VIA_STRING(VIA_SERVER_RESULT, VIA_SERVER_NOT_MODIFIED);
SET_VIA_STRING(VIA_PROXY_RESULT, VIA_PROXY_NOT_MODIFIED);
...
break;
case HTTP_STATUS_HTTPVER_NOT_SUPPORTED:
...
return;
case HTTP_STATUS_PARTIAL_CONTENT:
// If we get this back we should be just passing it through.
ink_assert(s->cache_info.action == CACHE_DO_NO_ACTION);
s->next_action = SM_ACTION_SERVER_READ;
break; // <-- no SET_VIA_STRING
default:
TxnDebug("http_trans", "[hncoofsr] server sent back something other than 100,304,200");
/* Default behavior is to pass-through response to the client */
ink_assert(s->cache_info.action == CACHE_DO_NO_ACTION);
s->next_action = SM_ACTION_SERVER_READ;
break; // <-- no SET_VIA_STRING
}
For comparison, the cached path (handle_cache_operation_on_forward_server_response(), same file, ~line 4435) does set both fields for the same default branch:
default:
TxnDebug("http_trans", "[hcoofsr] response code: %d", server_response_code);
SET_VIA_STRING(VIA_SERVER_RESULT, VIA_SERVER_SERVED);
SET_VIA_STRING(VIA_PROXY_RESULT, VIA_PROXY_SERVED);
...
So the no-cache path is simply inconsistent with the cache path.
VIA_CACHE_FILL_ACTION staying empty in this scenario is correct: cache_info.action == CACHE_DO_NO_ACTION falls through the default of the cache-action switch later and no fill action ever happens. That is by design and not part of this report.
Proposed fix
Update the no-cache path so that the Via debug field accurately reflects that the origin actually answered. Minimal patch (untested, against 9.2.x):
--- a/proxy/http/HttpTransact.cc
+++ b/proxy/http/HttpTransact.cc
@@ -4791,16 +4851,22 @@ HttpTransact::handle_no_cache_operation_on_forward_server_response(State *s)
case HTTP_STATUS_PARTIAL_CONTENT:
// If we get this back we should be just passing it through.
ink_assert(s->cache_info.action == CACHE_DO_NO_ACTION);
+ SET_VIA_STRING(VIA_SERVER_RESULT, VIA_SERVER_SERVED);
+ SET_VIA_STRING(VIA_PROXY_RESULT, VIA_PROXY_SERVED);
s->next_action = SM_ACTION_SERVER_READ;
break;
default:
TxnDebug("http_trans", "[hncoofsr] server sent back something other than 100,304,200");
/* Default behavior is to pass-through response to the client */
ink_assert(s->cache_info.action == CACHE_DO_NO_ACTION);
+ SET_VIA_STRING(VIA_SERVER_RESULT, VIA_SERVER_SERVED);
+ SET_VIA_STRING(VIA_PROXY_RESULT, VIA_PROXY_SERVED);
s->next_action = SM_ACTION_SERVER_READ;
break;
}
After the patch the visible Via window would become:
206 (Range, no cache write): [cMsSf ]
404 from origin (Range path): [cMsSf ]
- DNS failure synthesized
500: [cMs f ] (unchanged, since the server side never ran)
…which lets operators distinguish "origin returned this status" from "ATS synthesized an error" at a glance, just from insert_response_via_str = 2.
If the project prefers to leave default alone (because some non-2xx codes might have been generated locally by other code paths I'm not considering), then at least adding the two SET_VIA_STRING lines to the HTTP_STATUS_PARTIAL_CONTENT branch would already resolve the most common operator confusion.
Environment
- ATS version:
9.2.11 (also reproducible on other 9.2.x patch levels)
- OS: Ubuntu 22.04
proxy.config.http.insert_response_via_str = 2
proxy.config.http.cache.range.lookup = 1
proxy.config.http.cache.range.write = 0
- No relevant plugins (reproduces with a stock build + a single forward remap)
Additional notes
- Workaround for operators: bump
proxy.config.http.insert_response_via_str to 3 to expose the full 12-character via_string, including VIA_ERROR_TYPE. That at least lets you distinguish a DNS-failure-synthesized 500 (VIA_ERROR_TYPE = 'D') from a real origin response (VIA_ERROR_TYPE = 'N').
- Documentation impact: the admin guide section describing the
Via header should clarify that, prior to a fix, slot 5 (VIA_SERVER_RESULT) is not populated for 206/default when the transaction is forwarded without caching. Today this asymmetry is undocumented and surprising.
Happy to put up a PR with the diff above if the maintainers agree on the direction.
Summary
In ATS 9.2.x, when a transaction is routed through
HttpTransact::handle_no_cache_operation_on_forward_server_response()(i.e.cache_info.action == CACHE_DO_NO_ACTION, which is the typical path for Range requests whenproxy.config.http.cache.range.write = 0, for non-cacheable methods, when caching is disabled, etc.), thevia_stringslotsVIA_SERVER_RESULTandVIA_PROXY_RESULTare only updated for200 OKand304 Not Modified. For206 Partial Contentand for everything that falls into thedefaultbranch (404,5xx, …), the function does not callSET_VIA_STRING(...)at all.As a result, the response
Viaheader for these very common cases looks like:with two empty slots where
VIA_SERVER_RESULTandVIA_CACHE_FILL_ACTIONshould have been recorded. This makes theViaheader much less useful for operators trying to distinguish between "origin actually answered" vs. "internally synthesized error" (e.g. DNS failure →500 Cannot find server.) — both look identical atproxy.config.http.insert_response_via_str = 2.Affected version
9.2.x(verified against the current9.2.xbranch on GitHub, fileproxy/http/HttpTransact.cc)master/10.0.x.Reproduction
ATS configuration (defaults are sufficient — these are the relevant ones):
A simple remap to any HTTP origin, then:
Case A — Range request returning
206Response (excerpt):
Case B — Range request returning
404Case C — DNS failure, ATS-synthesized
500In all three cases the
Viadebug field is identical ([cMs f ]), even though Case A and Case B did contact the origin successfully and Case C never even attempted a server connection.Expected behavior
For Case A (
206) and Case B (404, going through thedefaultbranch), the origin did serve the response, so theViadebug field should reflect that:VIA_SERVER_RESULT = 'S'(VIA_SERVER_SERVED)VIA_PROXY_RESULT = 'S'(VIA_PROXY_SERVED)i.e. the field should look like
[cMsSf ]— consistent with what already happens for200/304.For Case C (DNS failure), the current
[cMs f ]is "expected" because no server connection was ever made, but theVIA_ERROR_TYPEslot does carry useful info ('D'forVIA_ERROR_DNS_FAILURE) — it is just not visible atinsert_response_via_str = 2because that level only emits the[VIA_CACHE, VIA_PROXY)window. That part is by design and not the subject of this issue, but it is worth mentioning that operators currently cannot tell Case A/B from Case C without bumping verbosity to3.Root cause (source pointers, 9.2.x)
The construction of the response
Viadebug window happens inHttpTransactHeaders::insert_via_header_in_response():So at verbosity
2the visible window covers exactly:VIA_CACHEVIA_CACHE_RESULTVIA_SERVERVIA_SERVER_RESULTVIA_CACHE_FILLVIA_CACHE_FILL_ACTIONvia_string[]is initialized inHttpTransact::State()(proxy/http/HttpTransact.h, ~line 830): all slots are filled with' ', then the six "marker" slots are overwritten with their fixed letters (u/c/s/f/p/e). Result slots are only set later when the correspondingSET_VIA_STRING(...)is executed.For Range requests with
cache_range_write = 0,HandleCacheOpenReadMiss()setsVIA_CACHE_RESULT = 'M'and forcescache_info.action = CACHE_DO_NO_ACTION:Because of
CACHE_DO_NO_ACTION,handle_response_from_server()then dispatches tohandle_no_cache_operation_on_forward_server_response():And here is the actual problem —
case HTTP_STATUS_PARTIAL_CONTENTanddefaultsimply don't update theViaslots:For comparison, the cached path (
handle_cache_operation_on_forward_server_response(), same file, ~line 4435) does set both fields for the samedefaultbranch:So the no-cache path is simply inconsistent with the cache path.
VIA_CACHE_FILL_ACTIONstaying empty in this scenario is correct:cache_info.action == CACHE_DO_NO_ACTIONfalls through thedefaultof the cache-actionswitchlater and no fill action ever happens. That is by design and not part of this report.Proposed fix
Update the no-cache path so that the
Viadebug field accurately reflects that the origin actually answered. Minimal patch (untested, against9.2.x):After the patch the visible
Viawindow would become:206(Range, no cache write):[cMsSf ]404from origin (Range path):[cMsSf ]500:[cMs f ](unchanged, since the server side never ran)…which lets operators distinguish "origin returned this status" from "ATS synthesized an error" at a glance, just from
insert_response_via_str = 2.If the project prefers to leave
defaultalone (because some non-2xx codes might have been generated locally by other code paths I'm not considering), then at least adding the twoSET_VIA_STRINGlines to theHTTP_STATUS_PARTIAL_CONTENTbranch would already resolve the most common operator confusion.Environment
9.2.11(also reproducible on other 9.2.x patch levels)proxy.config.http.insert_response_via_str = 2proxy.config.http.cache.range.lookup = 1proxy.config.http.cache.range.write = 0Additional notes
proxy.config.http.insert_response_via_strto3to expose the full 12-charactervia_string, includingVIA_ERROR_TYPE. That at least lets you distinguish a DNS-failure-synthesized 500 (VIA_ERROR_TYPE = 'D') from a real origin response (VIA_ERROR_TYPE = 'N').Viaheader should clarify that, prior to a fix, slot 5 (VIA_SERVER_RESULT) is not populated for206/defaultwhen the transaction is forwarded without caching. Today this asymmetry is undocumented and surprising.Happy to put up a PR with the diff above if the maintainers agree on the direction.