Skip to content
8 changes: 8 additions & 0 deletions api/envoy/config/core/v3/substitution_format_string.proto
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,12 @@ message SubstitutionFormatString {
// empty string, so that empty values are omitted entirely.
// * for ``json_format`` the keys with null values are omitted in the output structure.
bool omit_empty_values = 3;

// Specify a content_type for text_format. This will be ignored for json_format

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 .

//
// .. code-block::
//
// content_type: "text/html; charset=UTF-8"
//
string content_type = 4 [(validate.rules).string = {min_bytes: 1}];

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.

Do we need this validation since it is only valid for text_format?

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.

Also specify currently supported content type values are text/html; charset=UTF-8 and text/plain defaulted to text/plain ?

}

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

1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ New Features
* access log: added support for :ref:`%DOWNSTREAM_PEER_FINGERPRINT_1% <config_access_log_format_response_flags>` as a response flag.
* access log: added support for nested objects in :ref:`JSON logging mode <config_access_log_format_dictionaries>`.
* access log: added :ref:`omit_empty_values<envoy_v3_api_field_config.core.v3.SubstitutionFormatString.omit_empty_values>` option to omit unset value from formatted log.
* access log: added :ref:`content_type<envoy_v3_api_field_config.core.v3.SubstitutionFormatString.content_type>` option to set content-type for textformat.

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.

I think this is applicable for local reply (not so for access log) - so may be we should release note it under local_reply config?

* build: enable building envoy :ref:`arm64 images <arm_binaries>` by buildx tool in x86 CI platform.
* cluster: added new :ref:`connection_pool_per_downstream_connection <envoy_v3_api_field_config.cluster.v3.Cluster.connection_pool_per_downstream_connection>` flag, which enable creation of a new connection pool for each downstream connection.
* decompressor filter: reports compressed and uncompressed bytes in trailers.
Expand Down

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.

14 changes: 14 additions & 0 deletions source/common/formatter/substitution_format_string.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "common/formatter/substitution_format_string.h"

#include "common/formatter/substitution_formatter.h"
#include "common/http/headers.h"

namespace Envoy {
namespace Formatter {
Expand All @@ -25,5 +26,18 @@ FormatterPtr SubstitutionFormatStringUtils::fromProtoConfig(
return nullptr;
}

absl::string_view SubstitutionFormatStringUtils::getContentType(
const envoy::config::core::v3::SubstitutionFormatString& config) {
const std::string contentType = config.content_type();

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.

Simplify this as follows as those are the only ones supported now ?

if (contentType == Http::Headers::get().ContentTypeValues.Html)
    return Http::Headers::get().ContentTypeValues.Html;
  else
    return Http::Headers::get().ContentTypeValues.Text;

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.

Why can't we plumb through the content type directly, without having to rely on well-known values?

if (contentType == Http::Headers::get().ContentTypeValues.TextEventStream)
return Http::Headers::get().ContentTypeValues.TextEventStream;
else if (contentType == Http::Headers::get().ContentTypeValues.TextUtf8)
return Http::Headers::get().ContentTypeValues.TextUtf8;
else if (contentType == Http::Headers::get().ContentTypeValues.Html)
return Http::Headers::get().ContentTypeValues.Html;
else
return Http::Headers::get().ContentTypeValues.Text;
}

} // namespace Formatter
} // namespace Envoy
6 changes: 6 additions & 0 deletions source/common/formatter/substitution_format_string.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ class SubstitutionFormatStringUtils {
*/
static FormatterPtr createJsonFormatter(const ProtobufWkt::Struct& struct_format,
bool preserve_types, bool omit_empty_values);

/**
* Returns content-type from config SubstitutionFormatString
*/
static absl::string_view
getContentType(const envoy::config::core::v3::SubstitutionFormatString& config);
};

} // namespace Formatter
Expand Down
2 changes: 1 addition & 1 deletion source/common/local_reply/local_reply.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class BodyFormatter {
config.format_case() ==
envoy::config::core::v3::SubstitutionFormatString::FormatCase::kJsonFormat
? Http::Headers::get().ContentTypeValues.Json
: Http::Headers::get().ContentTypeValues.Text) {}
: Formatter::SubstitutionFormatStringUtils::getContentType(config)) {}

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.

So, given: https://github.com/envoyproxy/envoy/pull/13019/files#r488273786, here we can have: config.content_type().empty() ? Http::Headers::get().ContentTypeValues.Text : config.content_type().


void format(const Http::RequestHeaderMap& request_headers,
const Http::ResponseHeaderMap& response_headers,
Expand Down
10 changes: 10 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,15 @@ TEST_F(SubstitutionFormatStringUtilsTest, TestInvalidConfigs) {
}
}

TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigContentType) {
const std::string yaml = R"EOF(
text_format: "<h1>Sample html</h1>"
content_type: "text/html; charset=UTF-8"
)EOF";
TestUtility::loadFromYaml(yaml, config_);
const absl::string_view contentType = SubstitutionFormatStringUtils::getContentType(config_);
EXPECT_EQ("text/html; charset=UTF-8", contentType);
}

} // namespace Formatter
} // namespace Envoy
57 changes: 57 additions & 0 deletions test/common/local_reply/local_reply_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -336,5 +336,62 @@ TEST_F(LocalReplyTest, TestHeaderAddition) {
ASSERT_EQ(out[1], "append-bar3");
}

TEST_F(LocalReplyTest, TestMapperWithContentType) {
// 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:
text_format: "<h1>%LOCAL_REPLY_BODY%</h1>"
content_type: "text/html; charset=UTF-8"
- 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:
text_format: "<h1>%LOCAL_REPLY_BODY%</h1> %RESPONSE_CODE% default formatter"
content_type: "text/html; charset=UTF-8"
)";
TestUtility::loadFromYaml(yaml, config_);
auto local = Factory::create(config_, context_);

// code=400 matches the first filter; rewrite code and body
// has its own formatter.
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