Skip to content
10 changes: 10 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,14 @@ 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* field.
// If this field is not set then ``text/plain`` is used for *text_format* and
// ``application/json`` is used for *json_format*.
//
// .. code-block::
//
// content_type: "text/html; charset=UTF-8"
//
string content_type = 4;
}
10 changes: 10 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.

5 changes: 4 additions & 1 deletion docs/root/configuration/http/http_conn_man/local_reply.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ The response body content type can be customized. If not specified, the content

Local reply format can be specified as :ref:`SubstitutionFormatString <envoy_v3_api_msg_config.core.v3.SubstitutionFormatString>`. It supports :ref:`text_format <envoy_v3_api_field_config.core.v3.SubstitutionFormatString.text_format>` and :ref:`json_format <envoy_v3_api_field_config.core.v3.SubstitutionFormatString.json_format>`.

Optionally, content-type can be modified further via :ref:`content_type <envoy_v3_api_field_config.core.v3.SubstitutionFormatString.content_type>` field. If not specified, default content-type is `text/plain` for :ref:`text_format <envoy_v3_api_field_config.core.v3.SubstitutionFormatString.text_format>` and `application/json` for :ref:`json_format <envoy_v3_api_field_config.core.v3.SubstitutionFormatString.json_format>`.

Example of a LocalReplyConfig with `body_format` field.

.. code-block::
Expand All @@ -63,7 +65,8 @@ Example of a LocalReplyConfig with `body_format` field.
runtime_key: key_b
status_code: 401
body_format_override:
text_format: "%LOCAL_REPLY_BODY% %REQ(:path)%"
text_format: "<h1>%LOCAL_REPLY_BODY% %REQ(:path)%</h1>"
content_type: "text/html; charset=UTF-8"
- filter:
status_code_filter:
comparison:
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ New Features
* load balancer: added a :ref:`configuration<envoy_v3_api_msg_config.cluster.v3.Cluster.LeastRequestLbConfig>` option to specify the active request bias used by the least request load balancer.
* load balancer: added an :ref:`option <envoy_v3_api_field_config.cluster.v3.Cluster.LbSubsetConfig.LbSubsetSelector.single_host_per_subset>` to optimize subset load balancing when there is only one host per subset.
* load balancer: added support for bounded load per host for consistent hash load balancers via :ref:`hash_balance_factor <envoy_api_field_Cluster.CommonLbConfig.consistent_hashing_lb_config>`.
* local_reply config: added :ref:`content_type<envoy_v3_api_field_config.core.v3.SubstitutionFormatString.content_type>` field to set content-type.
* lua: added Lua APIs to access :ref:`SSL connection info <config_http_filters_lua_ssl_socket_info>` object.
* lua: added Lua API for :ref:`base64 escaping a string <config_http_filters_lua_stream_handle_api_base64_escape>`.
* lua: added new :ref:`source_code <envoy_v3_api_field_extensions.filters.http.lua.v3.LuaPerRoute.source_code>` field to support the dispatching of inline Lua code in per route configuration of Lua filter.
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.

12 changes: 7 additions & 5 deletions source/common/local_reply/local_reply.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ class BodyFormatter {
BodyFormatter(const envoy::config::core::v3::SubstitutionFormatString& config)
: formatter_(Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config)),
content_type_(
config.format_case() ==
envoy::config::core::v3::SubstitutionFormatString::FormatCase::kJsonFormat
? Http::Headers::get().ContentTypeValues.Json
: Http::Headers::get().ContentTypeValues.Text) {}
!config.content_type().empty()
? config.content_type()
: config.format_case() ==
envoy::config::core::v3::SubstitutionFormatString::FormatCase::kJsonFormat
? Http::Headers::get().ContentTypeValues.Json
: Http::Headers::get().ContentTypeValues.Text) {}

void format(const Http::RequestHeaderMap& request_headers,
const Http::ResponseHeaderMap& response_headers,
Expand All @@ -40,7 +42,7 @@ class BodyFormatter {

private:
const Formatter::FormatterPtr formatter_;
const absl::string_view content_type_;
const std::string content_type_;
};

using BodyFormatterPtr = std::unique_ptr<BodyFormatter>;
Expand Down
80 changes: 80 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,85 @@ 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"
- filter:
status_code_filter:
comparison:
op: EQ
value:
default_value: 420
runtime_key: key_b
status_code: 421
body:
inline_string: "421 body text"
body_format_override:
text_format: "%LOCAL_REPLY_BODY%"
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.
// content-type is explicitly set to text/html; charset=UTF-8.
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(body_, "<h1>401 body text</h1>");
EXPECT_EQ(content_type_, "text/html; charset=UTF-8");

// code=410 matches the second filter; rewrite code and body
// but using default formatter.
// content-type is explicitly set to text/html; charset=UTF-8.
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");

// code=420 matches the third filter; rewrite code and body
// has its own formatter.
// default content-type is set based on reply format type.
resetData(420);
local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_);
EXPECT_EQ(code_, static_cast<Http::Code>(421));
EXPECT_EQ(stream_info_.response_code_, 421U);
EXPECT_EQ(response_headers_.Status()->value().getStringView(), "421");
EXPECT_EQ(body_, "421 body text");
EXPECT_EQ(content_type_, "text/plain");
}

} // namespace LocalReply
} // namespace Envoy