blob: b978e2188dadc2904d25c06356e4ac99c569dbad [file]
/** @file
Unit Tests for HttpTransact
@section license License
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.
*/
#include <string_view>
#include <vector>
#include <cstdio>
using namespace std::string_view_literals;
#include "tscore/Diags.h"
#include "tsutil/PostScript.h"
#include "proxy/http/HttpConfig.h"
#include "proxy/http/HttpTransact.h"
#include "proxy/http/remap/RemapProcessor.h"
#include "records/RecordsConfig.h"
#include <catch2/catch_test_macros.hpp>
TEST_CASE("HttpTransact", "[http]")
{
url_init();
mime_init();
http_init();
SECTION("RemapProcessor tolerates a missing remap table")
{
HttpTransact::State state;
RemapProcessor processor;
CHECK_FALSE(processor.setup_for_remap(&state, nullptr));
CHECK_FALSE(processor.finish_remap(&state, nullptr));
}
SECTION("HttpTransact::merge_response_header_with_cached_header")
{
SECTION("Basic")
{
HTTPHdr hdr1;
HTTPHdr hdr2;
ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });
MIMEField *field;
struct header {
std::string_view name;
std::string_view value;
};
struct header input1[] = {
{"AAA", "111"},
{"BBB", "222"},
{"CCC", "333"},
};
struct header input2[] = {
{"DDD", "444"},
{"EEE", "555"},
{"FFF", "666"}
};
hdr1.create(HTTPType::RESPONSE);
for (auto &&entry : input1) {
field = hdr1.field_create(entry.name);
hdr1.field_attach(field);
hdr1.field_value_set(field, entry.value.data(), entry.value.length());
}
hdr2.create(HTTPType::RESPONSE);
for (auto &&entry : input2) {
field = hdr2.field_create(entry.name);
hdr2.field_attach(field);
hdr2.field_value_set(field, entry.value.data(), entry.value.length());
}
HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);
CHECK(hdr1.fields_count() == 6);
field = hdr1.field_find("AAA"sv);
REQUIRE(field != nullptr);
auto str{field->value_get()};
CHECK(str == "111"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("BBB"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "222"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("CCC"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "333"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("DDD"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "444"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("EEE"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "555"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("FFF"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "666"sv);
CHECK(field->has_dups() == false);
}
SECTION("Have common headers")
{
HTTPHdr hdr1;
HTTPHdr hdr2;
ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });
MIMEField *field;
struct header {
std::string_view name;
std::string_view value;
};
struct header input1[] = {
{"AAA", "111"},
{"BBB", "222"},
{"CCC", "333"},
};
struct header input2[] = {
{"DDD", "444"},
{"BBB", "555"},
{"FFF", "666"}
};
hdr1.create(HTTPType::RESPONSE);
for (auto &&entry : input1) {
field = hdr1.field_create(entry.name);
hdr1.field_attach(field);
hdr1.field_value_set(field, entry.value.data(), entry.value.length());
}
hdr2.create(HTTPType::RESPONSE);
for (auto &&entry : input2) {
field = hdr2.field_create(entry.name);
hdr2.field_attach(field);
hdr2.field_value_set(field, entry.value.data(), entry.value.length());
}
HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);
CHECK(hdr1.fields_count() == 5);
field = hdr1.field_find("AAA"sv);
REQUIRE(field != nullptr);
auto str{field->value_get()};
CHECK(str == "111"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("BBB"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "555"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("CCC"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "333"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("DDD"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "444"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("FFF"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "666"sv);
CHECK(field->has_dups() == false);
}
SECTION("Have dup headers")
{
HTTPHdr hdr1;
HTTPHdr hdr2;
ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });
MIMEField *field;
struct header {
std::string_view name;
std::string_view value;
};
struct header input1[] = {
{"AAA", "111"},
{"BBB", "222"},
{"CCC", "333"},
};
struct header input2[] = {
{"DDD", "444"},
{"EEE", "555"},
{"EEE", "666"}
};
hdr1.create(HTTPType::RESPONSE);
for (auto &&entry : input1) {
field = hdr1.field_create(entry.name);
hdr1.field_attach(field);
hdr1.field_value_set(field, entry.value.data(), entry.value.length());
}
hdr2.create(HTTPType::RESPONSE);
for (auto &&entry : input2) {
field = hdr2.field_create(entry.name);
hdr2.field_attach(field);
hdr2.field_value_set(field, entry.value.data(), entry.value.length());
}
HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);
CHECK(hdr1.fields_count() == 6);
field = hdr1.field_find("AAA"sv);
REQUIRE(field != nullptr);
auto str{field->value_get()};
CHECK(str == "111"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("BBB"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "222"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("CCC"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "333"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("DDD"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "444"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("EEE"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "555"sv);
CHECK(field->has_dups() == true);
}
SECTION("Connection-named header from 304 is not merged")
{
HTTPHdr cached_headers;
HTTPHdr response_headers;
ts::PostScript cached_headers_defer([&]() -> void { cached_headers.destroy(); });
ts::PostScript response_headers_defer([&]() -> void { response_headers.destroy(); });
MIMEField *field;
struct header {
std::string_view name;
std::string_view value;
};
struct header cached[] = {
{"AAA", "111"},
{"BBB", "222"},
};
struct header response[] = {
{"Connection", "X-Evil" },
{"X-Evil", "injected"},
{"CCC", "333" },
};
cached_headers.create(HTTPType::RESPONSE);
for (auto &&entry : cached) {
field = cached_headers.field_create(entry.name);
cached_headers.field_attach(field);
cached_headers.field_value_set(field, entry.value.data(), entry.value.length());
}
response_headers.create(HTTPType::RESPONSE);
for (auto &&entry : response) {
field = response_headers.field_create(entry.name);
response_headers.field_attach(field);
response_headers.field_value_set(field, entry.value.data(), entry.value.length());
}
HttpTransact::merge_response_header_with_cached_header(&cached_headers, &response_headers);
CHECK(cached_headers.fields_count() == 3);
field = cached_headers.field_find("Connection"sv);
CHECK(field == nullptr);
field = cached_headers.field_find("X-Evil"sv);
CHECK(field == nullptr);
field = cached_headers.field_find("CCC"sv);
REQUIRE(field != nullptr);
auto str{field->value_get()};
CHECK(str == "333"sv);
CHECK(field->has_dups() == false);
}
SECTION("Multiple Connection tokens are all skipped")
{
HTTPHdr cached_headers;
HTTPHdr response_headers;
ts::PostScript cached_headers_defer([&]() -> void { cached_headers.destroy(); });
ts::PostScript response_headers_defer([&]() -> void { response_headers.destroy(); });
MIMEField *field;
struct header {
std::string_view name;
std::string_view value;
};
struct header cached[] = {
{"AAA", "111"},
};
struct header response[] = {
{"Connection", "X-Foo, X-Bar"},
{"X-Foo", "a" },
{"X-Bar", "b" },
{"DDD", "444" },
};
cached_headers.create(HTTPType::RESPONSE);
for (auto &&entry : cached) {
field = cached_headers.field_create(entry.name);
cached_headers.field_attach(field);
cached_headers.field_value_set(field, entry.value.data(), entry.value.length());
}
response_headers.create(HTTPType::RESPONSE);
for (auto &&entry : response) {
field = response_headers.field_create(entry.name);
response_headers.field_attach(field);
response_headers.field_value_set(field, entry.value.data(), entry.value.length());
}
HttpTransact::merge_response_header_with_cached_header(&cached_headers, &response_headers);
CHECK(cached_headers.fields_count() == 2);
field = cached_headers.field_find("X-Foo"sv);
CHECK(field == nullptr);
field = cached_headers.field_find("X-Bar"sv);
CHECK(field == nullptr);
field = cached_headers.field_find("DDD"sv);
REQUIRE(field != nullptr);
auto str{field->value_get()};
CHECK(str == "444"sv);
CHECK(field->has_dups() == false);
}
SECTION("Connection: TE keeps cached TE and merges normal headers")
{
HTTPHdr cached_headers;
HTTPHdr response_headers;
ts::PostScript cached_headers_defer([&]() -> void { cached_headers.destroy(); });
ts::PostScript response_headers_defer([&]() -> void { response_headers.destroy(); });
MIMEField *field;
struct header {
std::string_view name;
std::string_view value;
};
struct header cached[] = {
{"AAA", "111" },
{"TE", "trailers"},
};
struct header response[] = {
{"Connection", "TE" },
{"TE", "trailers"},
{"BBB", "222" },
};
cached_headers.create(HTTPType::RESPONSE);
for (auto &&entry : cached) {
field = cached_headers.field_create(entry.name);
cached_headers.field_attach(field);
cached_headers.field_value_set(field, entry.value.data(), entry.value.length());
}
response_headers.create(HTTPType::RESPONSE);
for (auto &&entry : response) {
field = response_headers.field_create(entry.name);
response_headers.field_attach(field);
response_headers.field_value_set(field, entry.value.data(), entry.value.length());
}
HttpTransact::merge_response_header_with_cached_header(&cached_headers, &response_headers);
CHECK(cached_headers.fields_count() == 3);
field = cached_headers.field_find("TE"sv);
REQUIRE(field != nullptr);
auto str{field->value_get()};
CHECK(str == "trailers"sv);
CHECK(field->has_dups() == false);
field = cached_headers.field_find("BBB"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "222"sv);
CHECK(field->has_dups() == false);
}
SECTION("Have dup headers 2")
{
HTTPHdr hdr1;
HTTPHdr hdr2;
ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });
MIMEField *field;
struct header {
std::string_view name;
std::string_view value;
};
struct header input1[] = {
{"AAA", "111"},
{"BBB", "222"},
{"CCC", "333"},
};
struct header input2[] = {
{"DDD", "444"},
{"DDD", "555"},
{"FFF", "666"}
};
hdr1.create(HTTPType::RESPONSE);
for (auto &&entry : input1) {
field = hdr1.field_create(entry.name);
hdr1.field_attach(field);
hdr1.field_value_set(field, entry.value.data(), entry.value.length());
}
hdr2.create(HTTPType::RESPONSE);
for (auto &&entry : input2) {
field = hdr2.field_create(entry.name);
hdr2.field_attach(field);
hdr2.field_value_set(field, entry.value.data(), entry.value.length());
}
HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);
CHECK(hdr1.fields_count() == 6);
field = hdr1.field_find("AAA"sv);
REQUIRE(field != nullptr);
auto str{field->value_get()};
CHECK(str == "111"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("BBB"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "222"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("CCC"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "333"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("DDD"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "444"sv);
CHECK(field->has_dups() == true);
field = hdr1.field_find("FFF"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "666"sv);
CHECK(field->has_dups() == false);
}
SECTION("Have common and dup headers")
{
HTTPHdr hdr1;
HTTPHdr hdr2;
ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });
MIMEField *field;
struct header {
std::string_view name;
std::string_view value;
};
struct header input1[] = {
{"AAA", "111"},
{"BBB", "222"},
{"CCC", "333"},
{"DDD", "444"},
};
struct header input2[] = {
{"AAA", "555"},
{"BBB", "666"},
{"BBB", "777"},
{"CCC", "888"},
{"EEE", "999"},
};
hdr1.create(HTTPType::RESPONSE);
for (auto &&entry : input1) {
field = hdr1.field_create(entry.name);
hdr1.field_attach(field);
hdr1.field_value_set(field, entry.value.data(), entry.value.length());
}
hdr2.create(HTTPType::RESPONSE);
for (auto &&entry : input2) {
field = hdr2.field_create(entry.name);
hdr2.field_attach(field);
hdr2.field_value_set(field, entry.value.data(), entry.value.length());
}
HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);
CHECK(hdr1.fields_count() == 6);
field = hdr1.field_find("AAA"sv);
REQUIRE(field != nullptr);
auto str{field->value_get()};
CHECK(str == "555"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("BBB"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "666"sv);
CHECK(field->has_dups() == true);
///////////// Dup //////////////////////////
field = field->m_next_dup;
str = field->value_get();
CHECK(str == "777"sv);
CHECK(field->has_dups() == false);
///////////////////////////////////////
field = hdr1.field_find("CCC"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "888"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("DDD"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "444"sv);
CHECK(field->has_dups() == false);
field = hdr1.field_find("EEE"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "999"sv);
CHECK(field->has_dups() == false);
}
SECTION("Response has superset")
{
HTTPHdr cached_headers;
HTTPHdr response_headers;
ts::PostScript cached_headers_defer([&]() -> void { cached_headers.destroy(); });
ts::PostScript response_headers_defer([&]() -> void { response_headers.destroy(); });
MIMEField *field;
struct header {
std::string_view name;
std::string_view value;
};
struct header cached[] = {
{"Foo", "111"},
{"Fizz", "555"},
{"Bar", "333"},
{"Bop", "666"},
{"Bar", "222"},
{"X-Foo", "aaa"},
{"Eat", "444"},
};
// Response headers in a 304 should, in theory, match the cached headers, but, what if they don't?
// The response headers should still be merged into the cached object properly given the existing logic.
// In the following, the ordering is different from the cached headers, the Bar headers are missing, and two duplicate Zip
// headers are not in the cached object.
struct header response[] = {
{"X-Foo", "aaa"},
{"Zip", "888"},
{"Zip", "999"},
{"Eat", "444"},
{"Foo", "111"},
{"Fizz", "555"},
{"Bop", "666"},
};
cached_headers.create(HTTPType::RESPONSE);
for (auto &&entry : cached) {
field = cached_headers.field_create(entry.name);
cached_headers.field_attach(field);
cached_headers.field_value_set(field, entry.value.data(), entry.value.length());
}
response_headers.create(HTTPType::RESPONSE);
for (auto &&entry : response) {
field = response_headers.field_create(entry.name);
response_headers.field_attach(field);
response_headers.field_value_set(field, entry.value.data(), entry.value.length());
}
HttpTransact::merge_response_header_with_cached_header(&cached_headers, &response_headers);
CHECK(cached_headers.fields_count() == 9);
CHECK(response_headers.fields_count() == 7);
field = cached_headers.field_find("Foo"sv);
REQUIRE(field != nullptr);
auto str{field->value_get()};
CHECK(str == "111"sv);
CHECK(field->has_dups() == false);
field = cached_headers.field_find("Fizz"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "555"sv);
CHECK(field->has_dups() == false);
field = cached_headers.field_find("Bop"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "666"sv);
CHECK(field->has_dups() == false);
field = cached_headers.field_find("X-Foo"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "aaa"sv);
CHECK(field->has_dups() == false);
field = cached_headers.field_find("Eat"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "444"sv);
CHECK(field->has_dups() == false);
field = cached_headers.field_find("Bar"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "333"sv);
CHECK(field->has_dups() == true);
///////////// Dup //////////////////////////
field = field->m_next_dup;
str = field->value_get();
CHECK(str == "222"sv);
CHECK(field->has_dups() == false);
///////////////////////////////////////
field = cached_headers.field_find("Zip"sv);
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "888"sv);
CHECK(field->has_dups() == true);
///////////// Dup //////////////////////////
REQUIRE(field->m_next_dup != nullptr);
field = field->m_next_dup;
REQUIRE(field != nullptr);
str = field->value_get();
CHECK(str == "999"sv);
CHECK(field->has_dups() == false);
///////////////////////////////////////
}
// Regression: repeated 304 revalidation must not grow the cached header
// without bound (see merge_response_header_with_cached_header). The
// yardstick is HdrHeap::marshal_length() -- the size that gates the cache
// write -- NOT fields_count(): dead MIMEField slots grow the heap even
// while the live field count stays constant.
SECTION("Repeated revalidation does not grow the cached header heap")
{
struct header {
std::string_view name;
std::string_view value;
};
auto build = [](HTTPHdr &h, const std::vector<header> &fields) {
h.create(HTTPType::RESPONSE);
for (auto &&e : fields) {
MIMEField *f = h.field_create(e.name);
h.field_attach(f);
h.field_value_set(f, e.value);
}
};
// Merge `response` into a copy of `cached0` `iterations` times and return
// the growth in the cached header's marshalled heap size. A correct merge
// is idempotent, so the growth must be bounded (independent of iterations).
auto merge_growth = [&](const std::vector<header> &cached0, const std::vector<header> &response, int iterations) -> int {
HTTPHdr cached;
HTTPHdr resp;
ts::PostScript cached_defer([&]() -> void { cached.destroy(); });
ts::PostScript resp_defer([&]() -> void { resp.destroy(); });
build(cached, cached0);
build(resp, response);
// One warm-up merge so any one-time restructuring settles before measuring.
HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
int const before = cached.m_heap->marshal_length();
for (int i = 0; i < iterations; ++i) {
HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
}
return cached.m_heap->marshal_length() - before;
};
// No duplicated fields: pure in-place replacement, bounded.
CHECK(merge_growth(
{
{"Date", "d0"},
{"Expires", "e0"},
{"Etag", "t0"}
},
{{"Date", "d1"}, {"Expires", "e1"}, {"Etag", "t1"}}, 4000) <= 2048);
// A duplicated field (e.g. Cache-Control: max-age=60, public) ahead of
// single-valued fields must not force those single-valued fields down an
// appending path. Before the fix the sticky "dups_seen" flag did exactly
// that and this grew ~132 bytes per merge (megabytes over a day).
CHECK(merge_growth(
{
{"Cache-Control", "max-age=60"},
{"Cache-Control", "public" },
{"Expires", "e0" },
{"Etag", "t0" }
},
{{"Cache-Control", "max-age=60"}, {"Cache-Control", "public"}, {"Expires", "e0"}, {"Etag", "t0"}}, 4000) <= 2048);
// Response carries a duplicated field the cached copy does not yet have.
CHECK(merge_growth(
{
{"Date", "d0"},
{"Expires", "e0"},
{"Etag", "t0"}
},
{{"Date", "d0"}, {"Vary", "A"}, {"Vary", "B"}, {"Expires", "e0"}, {"Etag", "t0"}}, 4000) <= 2048);
// A response value that changes length every merge (as Expires does on a
// real 304) must still be bounded: value_set frees the old string (self-
// coalesced) and reuses the slot.
{
HTTPHdr cached;
HTTPHdr resp;
ts::PostScript cached_defer([&]() -> void { cached.destroy(); });
ts::PostScript resp_defer([&]() -> void { resp.destroy(); });
build(cached, {
{"Server", "ATS" },
{"Cache-Control", "max-age=60"},
{"Date", "d0" },
{"Expires", "e0" },
{"Etag", "t0" }
});
resp.create(HTTPType::RESPONSE);
MIMEField *rf = resp.field_create("Expires"sv);
resp.field_attach(rf);
resp.field_value_set(rf, "e0"sv);
HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
int const before = cached.m_heap->marshal_length();
char buf[64];
for (int i = 0; i < 2000; ++i) {
int n = std::snprintf(buf, sizeof(buf), "Sat, 18 Jul 2026 05:%02d:%02d GMT-%d", i % 60, (i * 7) % 60, i % 1000);
resp.field_value_set(rf, std::string_view(buf, n));
HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
}
MIMEField *exp = cached.field_find("Expires"sv);
REQUIRE(exp != nullptr);
CHECK(exp->has_dups() == false);
CHECK(cached.m_heap->marshal_length() - before <= 4096);
}
// Correctness: merging a response that DROPS one of two duplicate values
// must leave the cached header with exactly the response's values.
{
HTTPHdr cached;
HTTPHdr resp;
ts::PostScript cached_defer([&]() -> void { cached.destroy(); });
ts::PostScript resp_defer([&]() -> void { resp.destroy(); });
build(cached, {
{"Vary", "A" },
{"Vary", "B" },
{"Vary", "C" },
{"Etag", "t0"}
});
build(resp, {
{"Vary", "X" },
{"Vary", "Y" },
{"Etag", "t1"}
});
HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
int count = 0;
std::string_view v0, v1;
for (MIMEField *d = cached.field_find("Vary"sv); d != nullptr; d = d->m_next_dup) {
if (count == 0) {
v0 = d->value_get();
} else if (count == 1) {
v1 = d->value_get();
}
++count;
}
CHECK(count == 2); // surplus cached "C" removed
CHECK(v0 == "X"sv);
CHECK(v1 == "Y"sv);
MIMEField *etag = cached.field_find("Etag"sv);
REQUIRE(etag != nullptr);
CHECK(etag->value_get() == "t1"sv);
CHECK(etag->has_dups() == false);
}
// The production caller sequence: merge_and_update_headers_for_cache_update
// deletes a caching header only when the 304 OMITS it, then merges. This
// exercises the conditional-delete + the cooked WKS headers
// (Cache-Control/Expires/Age) end to end, over many revalidations with a
// changing Expires. The cached header heap and the cooked freshness values
// must stay correct and bounded.
{
HTTPHdr cached;
ts::PostScript cached_defer([&]() -> void { cached.destroy(); });
build(cached, {
{"Server", "ATS" },
{"Cache-Control", "max-age=60"},
{"Date", "d0" },
{"Expires", "e0" },
{"Etag", "t0" }
});
int settled = 0;
for (int i = 0; i < 2000; ++i) {
HTTPHdr resp;
ts::PostScript resp_defer([&]() -> void { resp.destroy(); });
resp.create(HTTPType::RESPONSE);
MIMEField *f = resp.field_create("Cache-Control"sv);
resp.field_attach(f);
resp.field_value_set(f, "max-age=60"sv);
char buf[64];
int n = std::snprintf(buf, sizeof(buf), "Sat, 18 Jul 2026 05:%02d:%02d GMT", i % 60, (i * 7) % 60);
f = resp.field_create("Expires"sv);
resp.field_attach(f);
resp.field_value_set(f, std::string_view(buf, n));
// Mirror merge_and_update_headers_for_cache_update: delete a caching
// header only if the response omits it, then merge.
if (!resp.presence(MIME_PRESENCE_AGE)) {
cached.field_delete(static_cast<std::string_view>(MIME_FIELD_AGE));
}
if (!resp.presence(MIME_PRESENCE_ETAG)) {
cached.field_delete(static_cast<std::string_view>(MIME_FIELD_ETAG));
}
if (!resp.presence(MIME_PRESENCE_EXPIRES)) {
cached.field_delete(static_cast<std::string_view>(MIME_FIELD_EXPIRES));
}
HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
if (i == 0) {
settled = cached.m_heap->marshal_length();
}
}
// Etag was omitted by every 304, so it must have been dropped from cache.
CHECK(cached.field_find("Etag"sv) == nullptr);
// Expires stays single-valued and the cooked max-age is intact.
MIMEField *exp = cached.field_find("Expires"sv);
REQUIRE(exp != nullptr);
CHECK(exp->has_dups() == false);
CHECK(cached.get_cooked_cc_max_age() == 60);
// Heap stays bounded across 2000 revalidations.
CHECK(cached.m_heap->marshal_length() - settled <= 4096);
}
}
}
SECTION("HttpTransact::strip_at_headers removes duplicate internal headers")
{
static constexpr char raw_request[] = "GET / HTTP/1.1\r\n"
"Host: example.test\r\n"
"@Ats-Internal: first\r\n"
"X-Keep: ok\r\n"
"@Ats-Internal: second\r\n"
"@Another: third\r\n"
"\r\n";
HTTPHdr hdr;
ts::PostScript hdr_defer([&]() -> void { hdr.destroy(); });
HTTPParser parser;
if (http_rsb.client_request_at_headers_stripped == nullptr) {
http_rsb.client_request_at_headers_stripped =
Metrics::Counter::createPtr("proxy.process.http.client_request_at_headers_stripped");
}
if (http_rsb.origin_response_at_headers_stripped == nullptr) {
http_rsb.origin_response_at_headers_stripped =
Metrics::Counter::createPtr("proxy.process.http.origin_response_at_headers_stripped");
}
hdr.create(HTTPType::REQUEST);
http_parser_init(&parser);
auto *start = raw_request;
auto const *end = raw_request + sizeof(raw_request) - 1;
ParseResult err;
while (true) {
err = hdr.parse_req(&parser, &start, end, true);
if (err != ParseResult::CONT) {
break;
}
}
REQUIRE(err == ParseResult::DONE);
HttpTransact::strip_at_headers(hdr, HttpTransact::AtHeaderSource::CLIENT_REQUEST, 1);
CHECK(hdr.field_find("@Ats-Internal"sv) == nullptr);
CHECK(hdr.field_find("@Another"sv) == nullptr);
CHECK(hdr.field_find("Host"sv) != nullptr);
MIMEField *field = hdr.field_find("X-Keep"sv);
REQUIRE(field != nullptr);
CHECK(field->value_get() == "ok"sv);
}
}