Skip to content
15 changes: 15 additions & 0 deletions api/envoy/config/core/v3/substitution_format_string.proto
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ message SubstitutionFormatString {
// }
//
google.protobuf.Struct json_format = 2 [(validate.rules).message = {required: true}];

// Specify a format with command operators to form a html string.
// Its details is described in :ref:`format string<config_access_log_format_strings>`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update these docs with a description of HTML format.

//
// .. code-block::
//
// html_format: <p>%LOCAL_REPLY_BODY%:%RESPONSE_CODE%:path=$REQ(:path)%</p>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, the only difference is that you are setting content type? Would it be preferable to be able to just specify the content-type?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@htuch - do you mean adding just content-type to ResponseMapper as is suggested here -> #13019 (comment) ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I'm thinking there's probably a lot of different variations on content type that might be asked for over time, so adding a special purpose field here is limiting. It makes sense to distinguish text/JSON, since they are structurally different.

//
// The following html text will be created:
//
// .. code-block::
//
// <p>upstream connect error:204:path=/foo</p>
//
string html_format = 4 [(validate.rules).string = {min_bytes: 1}];
}

// If set to true, when command operators are evaluated to null,
Expand Down
15 changes: 15 additions & 0 deletions api/envoy/config/core/v4alpha/substitution_format_string.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions source/common/formatter/substitution_format_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ FormatterPtr SubstitutionFormatStringUtils::fromProtoConfig(
switch (config.format_case()) {
case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kTextFormat:
return std::make_unique<FormatterImpl>(config.text_format(), config.omit_empty_values());
case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kHtmlFormat:
return std::make_unique<FormatterImpl>(config.html_format());
case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kJsonFormat: {
return createJsonFormatter(config.json_format(), true, config.omit_empty_values());
}
Expand Down
5 changes: 4 additions & 1 deletion source/common/local_reply/local_reply.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ class BodyFormatter {
config.format_case() ==
envoy::config::core::v3::SubstitutionFormatString::FormatCase::kJsonFormat
? Http::Headers::get().ContentTypeValues.Json
: Http::Headers::get().ContentTypeValues.Text) {}
: config.format_case() ==
Comment thread
dio marked this conversation as resolved.
envoy::config::core::v3::SubstitutionFormatString::FormatCase::kHtmlFormat
? Http::Headers::get().ContentTypeValues.Html
: Http::Headers::get().ContentTypeValues.Text) {}

void format(const Http::RequestHeaderMap& request_headers,
const Http::ResponseHeaderMap& response_headers,
Expand Down
12 changes: 12 additions & 0 deletions test/common/formatter/substitution_format_string_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,17 @@ TEST_F(SubstitutionFormatStringUtilsTest, TestInvalidConfigs) {
}
}

TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigHtml) {
const std::string yaml = R"EOF(
html_format: "<h1>Sample html</h1>"
)EOF";
TestUtility::loadFromYaml(yaml, config_);

auto formatter = SubstitutionFormatStringUtils::fromProtoConfig(config_);
const auto out = formatter->format(request_headers_, response_headers_, response_trailers_,
stream_info_, body_);
EXPECT_EQ("<h1>Sample html</h1>", out);
}

} // namespace Formatter
} // namespace Envoy
73 changes: 73 additions & 0 deletions test/common/local_reply/local_reply_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,24 @@ TEST_F(LocalReplyTest, TestDefaultJsonFormatter) {
EXPECT_TRUE(TestUtility::jsonStringEqual(body_, expected));
}

TEST_F(LocalReplyTest, TestDefaultHtmlFormatter) {
// Default html formatter without any mappers
const std::string yaml = R"(
body_format:
html_format: "<h1>Sample Html Body</h1>"
)";
TestUtility::loadFromYaml(yaml, config_);
auto local = Factory::create(config_, context_);

local->rewrite(nullptr, response_headers_, stream_info_, code_, body_, content_type_);
EXPECT_EQ(code_, TestInitCode);
EXPECT_EQ(stream_info_.response_code_, static_cast<uint32_t>(TestInitCode));
EXPECT_EQ(response_headers_.Status()->value().getStringView(),
std::to_string(enumToInt(TestInitCode)));
EXPECT_EQ(body_, "<h1>Sample Html Body</h1>");
EXPECT_EQ(content_type_, "text/html; charset=UTF-8");
}

TEST_F(LocalReplyTest, TestMapperRewrite) {
// Match with response_code, and rewrite the code and body.
const std::string yaml = R"(
Expand Down Expand Up @@ -336,5 +354,60 @@ TEST_F(LocalReplyTest, TestHeaderAddition) {
ASSERT_EQ(out[1], "append-bar3");
}

TEST_F(LocalReplyTest, TestMapperWithHtmlFormat) {
// Match with response_code, and rewrite the code and body.
const std::string yaml = R"(
mappers:
- filter:
status_code_filter:
comparison:
op: EQ
value:
default_value: 400
runtime_key: key_b
status_code: 401
body:
inline_string: "401 body text"
body_format_override:
html_format: "<h1>%LOCAL_REPLY_BODY%</h1>"
- filter:
status_code_filter:
comparison:
op: EQ
value:
default_value: 410
runtime_key: key_b
status_code: 411
body:
inline_string: "411 body text"
body_format:
html_format: "<h1>%LOCAL_REPLY_BODY%</h1> %RESPONSE_CODE% default formatter"
)";
TestUtility::loadFromYaml(yaml, config_);
auto local = Factory::create(config_, context_);

// code=400 matches the first filter; rewrite code and body
// has its own formatter

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit end with .

resetData(400);
local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_);
EXPECT_EQ(code_, static_cast<Http::Code>(401));
EXPECT_EQ(stream_info_.response_code_, 401U);
EXPECT_EQ(response_headers_.Status()->value().getStringView(), "401");
EXPECT_EQ(content_type_, "text/html; charset=UTF-8");

const std::string expected = R"(<h1>401 body text</h1>)";
EXPECT_EQ(body_, expected);

// code=410 matches the second filter; rewrite code and body
Comment thread
dio marked this conversation as resolved.
// but using default formatter
resetData(410);
local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_);
EXPECT_EQ(code_, static_cast<Http::Code>(411));
EXPECT_EQ(stream_info_.response_code_, 411U);
EXPECT_EQ(response_headers_.Status()->value().getStringView(), "411");
EXPECT_EQ(body_, "<h1>411 body text</h1> 411 default formatter");
EXPECT_EQ(content_type_, "text/html; charset=UTF-8");
}

} // namespace LocalReply
} // namespace Envoy