diff --git a/core/functional_tests/http2server/static_config.yaml b/core/functional_tests/http2server/static_config.yaml index 2593a497ae9d..c29f3a631589 100644 --- a/core/functional_tests/http2server/static_config.yaml +++ b/core/functional_tests/http2server/static_config.yaml @@ -74,7 +74,7 @@ components_manager: handler-http2: path: /http2server - method: GET,POST,PUT,DELETE,HEAD + method: GET,POST,PUT,DELETE,HEAD,TRACE task_processor: main-task-processor throttling_enabled: false max_request_size: 2097152 # 2Mib diff --git a/core/functional_tests/http2server/tests/conftest.py b/core/functional_tests/http2server/tests/conftest.py index 945444423478..f90760eac44f 100644 --- a/core/functional_tests/http2server/tests/conftest.py +++ b/core/functional_tests/http2server/tests/conftest.py @@ -161,6 +161,25 @@ async def head( timeout, ) + async def trace( + self, + path, + params={}, + headers={}, + data=None, + json={}, + timeout=DEFAULT_TIMEOUT, + ) -> httpx.Response: + return await self._request( + 'TRACE', + path, + params, + headers, + data, + json, + timeout, + ) + async def _request( self, method, diff --git a/core/functional_tests/http2server/tests/test_high_level.py b/core/functional_tests/http2server/tests/test_high_level.py index 37001fe46fa8..2ab919911e41 100644 --- a/core/functional_tests/http2server/tests/test_high_level.py +++ b/core/functional_tests/http2server/tests/test_high_level.py @@ -73,6 +73,19 @@ async def test_headers(http2_client): assert hval == r.text +async def test_trace_is_routed(http2_client): + # TRACE arrives as a ':method' pseudo-header, so unlike HTTP/1.1 it never + # goes through llhttp; the handler must be reached over HTTP/2 as well. + hval = 'traced' + r = await http2_client.trace( + DEFAULT_PATH, + params={'type': 'echo-header'}, + headers={'echo-header': hval}, + ) + assert 200 == r.status_code + assert hval == r.text + + async def test_head_response_has_no_body(http2_client): r = await http2_client.head( DEFAULT_PATH, diff --git a/core/include/userver/clients/http/request.hpp b/core/include/userver/clients/http/request.hpp index 0d6b9d771a4e..c4396507b554 100644 --- a/core/include/userver/clients/http/request.hpp +++ b/core/include/userver/clients/http/request.hpp @@ -48,7 +48,7 @@ class EasyWrapper; } // namespace impl /// @brief HTTP request method -enum class HttpMethod { kGet, kPost, kHead, kPut, kDelete, kPatch, kOptions }; +enum class HttpMethod { kGet, kPost, kHead, kPut, kDelete, kPatch, kOptions, kTrace }; /// @brief Convert HTTP method enum value to string std::string_view ToStringView(HttpMethod method); diff --git a/core/include/userver/server/http/http_method.hpp b/core/include/userver/server/http/http_method.hpp index 5481511b6833..bac1c175f03e 100644 --- a/core/include/userver/server/http/http_method.hpp +++ b/core/include/userver/server/http/http_method.hpp @@ -27,6 +27,7 @@ enum class HttpMethod { kPatch, kConnect, kOptions, + kTrace, kUnknown, }; diff --git a/core/src/clients/http/http_method_test.cpp b/core/src/clients/http/http_method_test.cpp index 3f6b3c3d2d20..47220051ed08 100644 --- a/core/src/clients/http/http_method_test.cpp +++ b/core/src/clients/http/http_method_test.cpp @@ -17,6 +17,7 @@ UTEST(ClientHttpMethodTest, Convert) { EXPECT_EQ(ch::ToStringView(ch::HttpMethod::kPut), "PUT"); EXPECT_EQ(ch::ToStringView(ch::HttpMethod::kDelete), "DELETE"); EXPECT_EQ(ch::ToStringView(ch::HttpMethod::kOptions), "OPTIONS"); + EXPECT_EQ(ch::ToStringView(ch::HttpMethod::kTrace), "TRACE"); EXPECT_EQ(ch::HttpMethodFromString("GET"), ch::HttpMethod::kGet); EXPECT_EQ(ch::HttpMethodFromString("HEAD"), ch::HttpMethod::kHead); @@ -25,6 +26,7 @@ UTEST(ClientHttpMethodTest, Convert) { EXPECT_EQ(ch::HttpMethodFromString("PUT"), ch::HttpMethod::kPut); EXPECT_EQ(ch::HttpMethodFromString("DELETE"), ch::HttpMethod::kDelete); EXPECT_EQ(ch::HttpMethodFromString("OPTIONS"), ch::HttpMethod::kOptions); + EXPECT_EQ(ch::HttpMethodFromString("TRACE"), ch::HttpMethod::kTrace); UEXPECT_THROW(ch::HttpMethodFromString("123"), std::runtime_error); } diff --git a/core/src/clients/http/request.cpp b/core/src/clients/http/request.cpp index f9f02d4cb868..43e3c7be2566 100644 --- a/core/src/clients/http/request.cpp +++ b/core/src/clients/http/request.cpp @@ -43,7 +43,8 @@ constexpr utils::TrivialBiMap kHttpMethodMap([](auto selector) { .Case(HttpMethod::kPut, "PUT") .Case(HttpMethod::kPatch, "PATCH") .Case(HttpMethod::kDelete, "DELETE") - .Case(HttpMethod::kOptions, "OPTIONS"); + .Case(HttpMethod::kOptions, "OPTIONS") + .Case(HttpMethod::kTrace, "TRACE"); }); curl::easy::http_version_t ToNative(HttpVersion version) { diff --git a/core/src/clients/http/request_state.cpp b/core/src/clients/http/request_state.cpp index 2560ea981e37..a80e7d40fa4d 100644 --- a/core/src/clients/http/request_state.cpp +++ b/core/src/clients/http/request_state.cpp @@ -754,6 +754,7 @@ void RequestState::SetMethod(HttpMethod method) { switch (method) { case HttpMethod::kDelete: case HttpMethod::kOptions: + case HttpMethod::kTrace: easy().set_custom_request(ToString(method)); break; case HttpMethod::kGet: diff --git a/core/src/server/http/handler_methods.hpp b/core/src/server/http/handler_methods.hpp index ba0ead79a732..01080a01e86a 100644 --- a/core/src/server/http/handler_methods.hpp +++ b/core/src/server/http/handler_methods.hpp @@ -16,6 +16,7 @@ inline constexpr HttpMethod kHandlerMethods[] = { HttpMethod::kDelete, HttpMethod::kPatch, HttpMethod::kOptions, + HttpMethod::kTrace, HttpMethod::kUnknown }; diff --git a/core/src/server/http/http_method.cpp b/core/src/server/http/http_method.cpp index 3e9889d9a24a..e8783bcbd59e 100644 --- a/core/src/server/http/http_method.cpp +++ b/core/src/server/http/http_method.cpp @@ -19,6 +19,7 @@ struct HttpMethodStrings { const std::string kConnect = "CONNECT"; const std::string kPatch = "PATCH"; const std::string kOptions = "OPTIONS"; + const std::string kTrace = "TRACE"; const std::string kUnknown = "unknown"; }; // NOLINTEND(readability-identifier-naming) @@ -81,6 +82,11 @@ HttpMethod HttpMethodFromString(std::string_view method_str) { result = HttpMethod::kOptions; } break; + case 'T': + if (method_str == strings.kTrace) { + result = HttpMethod::kTrace; + } + break; } } @@ -116,6 +122,8 @@ const std::string& ToString(HttpMethod method) noexcept { return strings.kPatch; case HttpMethod::kOptions: return strings.kOptions; + case HttpMethod::kTrace: + return strings.kTrace; case HttpMethod::kUnknown: return strings.kUnknown; } diff --git a/core/src/server/http/http_method_test.cpp b/core/src/server/http/http_method_test.cpp new file mode 100644 index 000000000000..8cb490c5e04c --- /dev/null +++ b/core/src/server/http/http_method_test.cpp @@ -0,0 +1,51 @@ +#include + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace { +namespace sh = server::http; +} + +UTEST(ServerHttpMethodTest, ToString) { + EXPECT_EQ(sh::ToString(sh::HttpMethod::kDelete), "DELETE"); + EXPECT_EQ(sh::ToString(sh::HttpMethod::kGet), "GET"); + EXPECT_EQ(sh::ToString(sh::HttpMethod::kHead), "HEAD"); + EXPECT_EQ(sh::ToString(sh::HttpMethod::kPost), "POST"); + EXPECT_EQ(sh::ToString(sh::HttpMethod::kPut), "PUT"); + EXPECT_EQ(sh::ToString(sh::HttpMethod::kPatch), "PATCH"); + EXPECT_EQ(sh::ToString(sh::HttpMethod::kConnect), "CONNECT"); + EXPECT_EQ(sh::ToString(sh::HttpMethod::kOptions), "OPTIONS"); + EXPECT_EQ(sh::ToString(sh::HttpMethod::kTrace), "TRACE"); +} + +UTEST(ServerHttpMethodTest, FromString) { + EXPECT_EQ(sh::HttpMethodFromString("DELETE"), sh::HttpMethod::kDelete); + EXPECT_EQ(sh::HttpMethodFromString("GET"), sh::HttpMethod::kGet); + EXPECT_EQ(sh::HttpMethodFromString("HEAD"), sh::HttpMethod::kHead); + EXPECT_EQ(sh::HttpMethodFromString("POST"), sh::HttpMethod::kPost); + EXPECT_EQ(sh::HttpMethodFromString("PUT"), sh::HttpMethod::kPut); + EXPECT_EQ(sh::HttpMethodFromString("PATCH"), sh::HttpMethod::kPatch); + EXPECT_EQ(sh::HttpMethodFromString("CONNECT"), sh::HttpMethod::kConnect); + EXPECT_EQ(sh::HttpMethodFromString("OPTIONS"), sh::HttpMethod::kOptions); + EXPECT_EQ(sh::HttpMethodFromString("TRACE"), sh::HttpMethod::kTrace); + + UEXPECT_THROW(sh::HttpMethodFromString("TRAC"), std::runtime_error); + UEXPECT_THROW(sh::HttpMethodFromString("TRACER"), std::runtime_error); + UEXPECT_THROW(sh::HttpMethodFromString("trace"), std::runtime_error); +} + +// TRACE must be registrable in a handler `method:` list, unlike CONNECT, which +// userver never routes to a handler. +UTEST(ServerHttpMethodTest, IsHandlerMethod) { + EXPECT_TRUE(sh::IsHandlerMethod(sh::HttpMethod::kGet)); + EXPECT_TRUE(sh::IsHandlerMethod(sh::HttpMethod::kOptions)); + EXPECT_TRUE(sh::IsHandlerMethod(sh::HttpMethod::kTrace)); + + EXPECT_FALSE(sh::IsHandlerMethod(sh::HttpMethod::kConnect)); +} + +USERVER_NAMESPACE_END diff --git a/core/src/server/http/http_request_method_test.cpp b/core/src/server/http/http_request_method_test.cpp index 6eb9ff410ead..2bda89aa7b16 100644 --- a/core/src/server/http/http_request_method_test.cpp +++ b/core/src/server/http/http_request_method_test.cpp @@ -40,6 +40,7 @@ INSTANTIATE_UTEST_SUITE_P( MethodsData{"CONNECT", HttpMethod::kConnect}, MethodsData{"PATCH", HttpMethod::kPatch}, MethodsData{"OPTIONS", HttpMethod::kOptions}, + MethodsData{"TRACE", HttpMethod::kTrace}, MethodsData{"GE", HttpMethod::kUnknown}, MethodsData{"GETT", HttpMethod::kUnknown}, MethodsData{"get", HttpMethod::kUnknown}, diff --git a/core/src/server/http/http_request_parser.cpp b/core/src/server/http/http_request_parser.cpp index be2d6fab3ce1..913dcb76d6ae 100644 --- a/core/src/server/http/http_request_parser.cpp +++ b/core/src/server/http/http_request_parser.cpp @@ -58,6 +58,8 @@ HttpMethod ConvertHttpMethod(llhttp_method method) { return HttpMethod::kPatch; case HTTP_OPTIONS: return HttpMethod::kOptions; + case HTTP_TRACE: + return HttpMethod::kTrace; default: return HttpMethod::kUnknown; } diff --git a/core/utest/src/utest/http_server_mock.cpp b/core/utest/src/utest/http_server_mock.cpp index ac80c24bfaee..d5c46721a12e 100644 --- a/core/utest/src/utest/http_server_mock.cpp +++ b/core/utest/src/utest/http_server_mock.cpp @@ -29,6 +29,8 @@ clients::http::HttpMethod ConvertHttpMethod(llhttp_method method) { return clients::http::HttpMethod::kPatch; case HTTP_OPTIONS: return clients::http::HttpMethod::kOptions; + case HTTP_TRACE: + return clients::http::HttpMethod::kTrace; default: ADD_FAILURE() << "Unknown HTTP method " << method; return clients::http::HttpMethod::kGet;