From 31419406cc2402b181933e11b2d4ccaa8d15fa0a Mon Sep 17 00:00:00 2001 From: Calle Wilund Date: Mon, 28 Jul 2025 13:17:04 +0000 Subject: [PATCH 1/3] http::reply::status_type: Promote to namespace and change to struct Moves HTTP status codes to top-level type "status", and changes it from enum to struct. The former to be able to forward declare the type. The latter to be able to type-safely extend the type (i.e. define a typed constant in a different compilation unit, without having to modify a seastar enum. --- include/seastar/http/reply.hh | 158 +++++++++++++++++++++++----------- src/http/reply.cc | 14 ++- 2 files changed, 117 insertions(+), 55 deletions(-) diff --git a/include/seastar/http/reply.hh b/include/seastar/http/reply.hh index ff0e6636184..2760bf8caf5 100644 --- a/include/seastar/http/reply.hh +++ b/include/seastar/http/reply.hh @@ -55,6 +55,94 @@ class routes; namespace http { +/** + * This type is moved to namespace level, so + * we can forward declare it. + * + * Wrapper type for HTTP status codes, including + * contants for the most common ones. + * + * Note: this was an enum, but changed to a + * struct wrapper type to make it extensible. + * + * This has the drawback of the type being + * weakly aliasable to int. This is however + * also a benefit. + */ +struct status_type { + int value; + + constexpr explicit status_type(int v) + : value(v) + {} + constexpr operator int() const { + return value; + } + std::strong_ordering operator<=>(const status_type& s) const = default; + + // Helper type to work around constexpr constants + // not being declarable inside their own type definition. + // + // Do not use this type directly. + struct status_init { + int value; + constexpr operator status_type() const { + return status_type(value); + } + constexpr operator int() const { + return value; + } + }; + + static constexpr status_init continue_{100}; //!< continue + static constexpr status_init switching_protocols{101}; //!< switching_protocols + static constexpr status_init ok{200}; //!< ok + static constexpr status_init created{201}; //!< created + static constexpr status_init accepted{202}; //!< accepted + static constexpr status_init nonauthoritative_information{203}; //!< nonauthoritative_information + static constexpr status_init no_content{204}; //!< no_content + static constexpr status_init reset_content{205}; //!< reset_content + static constexpr status_init partial_content{206}; //! partial_content + static constexpr status_init multiple_choices{300}; //!< multiple_choices + static constexpr status_init moved_permanently{301}; //!< moved_permanently + static constexpr status_init moved_temporarily{302}; //!< moved_temporarily + static constexpr status_init see_other{303}; //!< see_other + static constexpr status_init not_modified{304}; //!< not_modified + static constexpr status_init use_proxy{305}; //!< use_proxy + static constexpr status_init temporary_redirect{307}; //!< temporary_redirect + static constexpr status_init permanent_redirect{308}; //!< permanent_redirect + static constexpr status_init bad_request{400}; //!< bad_request + static constexpr status_init unauthorized{401}; //!< unauthorized + static constexpr status_init payment_required{402}; //!< payment_required + static constexpr status_init forbidden{403}; //!< forbidden + static constexpr status_init not_found{404}; //!< not_found + static constexpr status_init method_not_allowed{405}; //!< method_not_allowed + static constexpr status_init not_acceptable{406}; //!< not_acceptable + static constexpr status_init request_timeout{408}; //!< request_timeout + static constexpr status_init conflict{409}; //!< conflict + static constexpr status_init gone{410}; //!< gone + static constexpr status_init length_required{411}; //!< length_required + static constexpr status_init payload_too_large{413}; //!< payload_too_large + static constexpr status_init uri_too_long{414}; //!< uri_too_long + static constexpr status_init unsupported_media_type{415}; //!< unsupported_media_type + static constexpr status_init expectation_failed{417}; //!< expectation_failed + static constexpr status_init page_expired{419}; //!< page_expired + static constexpr status_init unprocessable_entity{422}; //!< unprocessable_entity + static constexpr status_init upgrade_required{426}; //!< upgrade_required + static constexpr status_init too_many_requests{429}; //!< too_many_requests + static constexpr status_init login_timeout{440}; //!< login_timeout + static constexpr status_init internal_server_error{500}; //!< internal_server_error + static constexpr status_init not_implemented{501}; //!< not_implemented + static constexpr status_init bad_gateway{502}; //!< bad_gateway + static constexpr status_init service_unavailable{503}; //!< service_unavailable + static constexpr status_init gateway_timeout{504}; //!< gateway_timeout + static constexpr status_init http_version_not_supported{505}; //!< http_version_not_supported + static constexpr status_init insufficient_storage{507}; //!< insufficient_storage + static constexpr status_init bandwidth_limit_exceeded{509}; //!< bandwidth_limit_exceeded + static constexpr status_init network_read_timeout{598}; //!< network_read_timeout + static constexpr status_init network_connect_timeout{599}; //!< network_connect_timeout +}; + /** * A reply to be sent to a client. */ @@ -62,55 +150,8 @@ struct reply { /** * The status of the reply. */ - enum class status_type { - continue_ = 100, //!< continue - switching_protocols = 101, //!< switching_protocols - ok = 200, //!< ok - created = 201, //!< created - accepted = 202, //!< accepted - nonauthoritative_information = 203, //!< nonauthoritative_information - no_content = 204, //!< no_content - reset_content = 205, //!< reset_content - partial_content = 206, //! partial_content - multiple_choices = 300, //!< multiple_choices - moved_permanently = 301, //!< moved_permanently - moved_temporarily = 302, //!< moved_temporarily - see_other = 303, //!< see_other - not_modified = 304, //!< not_modified - use_proxy = 305, //!< use_proxy - temporary_redirect = 307, //!< temporary_redirect - permanent_redirect = 308, //!< permanent_redirect - bad_request = 400, //!< bad_request - unauthorized = 401, //!< unauthorized - payment_required = 402, //!< payment_required - forbidden = 403, //!< forbidden - not_found = 404, //!< not_found - method_not_allowed = 405, //!< method_not_allowed - not_acceptable = 406, //!< not_acceptable - request_timeout = 408, //!< request_timeout - conflict = 409, //!< conflict - gone = 410, //!< gone - length_required = 411, //!< length_required - payload_too_large = 413, //!< payload_too_large - uri_too_long = 414, //!< uri_too_long - unsupported_media_type = 415, //!< unsupported_media_type - expectation_failed = 417, //!< expectation_failed - page_expired = 419, //!< page_expired - unprocessable_entity = 422, //!< unprocessable_entity - upgrade_required = 426, //!< upgrade_required - too_many_requests = 429, //!< too_many_requests - login_timeout = 440, //!< login_timeout - internal_server_error = 500, //!< internal_server_error - not_implemented = 501, //!< not_implemented - bad_gateway = 502, //!< bad_gateway - service_unavailable = 503, //!< service_unavailable - gateway_timeout = 504, //!< gateway_timeout - http_version_not_supported = 505, //!< http_version_not_supported - insufficient_storage = 507, //!< insufficient_storage - bandwidth_limit_exceeded = 509, //!< bandwidth_limit_exceeded - network_read_timeout = 598, //!< network_read_timeout - network_connect_timeout = 599, //!< network_connect_timeout - } _status; + using status_type = http::status_type; + status_type _status; /** * HTTP status classes @@ -137,7 +178,7 @@ struct reply { * @return one of the \ref status_class values */ static constexpr status_class classify_status(status_type http_status) { - auto sc = static_cast>(http_status) / 100; + auto sc = int(http_status) / 100; if (sc < 1 || sc > 5) [[unlikely]] { return status_class::unclassified; } @@ -287,7 +328,8 @@ private: friend class httpd::connection; }; -std::ostream& operator<<(std::ostream& os, reply::status_type st); +std::ostream& operator<<(std::ostream& os, status_type st); +std::ostream& operator<<(std::ostream& os, status_type::status_init st); } // namespace http @@ -299,5 +341,17 @@ SEASTAR_MODULE_EXPORT_END } #if FMT_VERSION >= 90000 -template <> struct fmt::formatter : fmt::ostream_formatter {}; +template <> struct fmt::formatter : fmt::ostream_formatter {}; +template <> struct fmt::formatter : fmt::formatter {}; #endif + +/** + * Temporary addition to enable existing code, using things + * like std::underlying_type_t for casts etc + * to continue worksing. This is not a fully kosher way of + * treating this overload, but it is mostly harmless... + */ +template<> +struct std::underlying_type { + using type = int; +}; diff --git a/src/http/reply.cc b/src/http/reply.cc index b9796621be5..2f410ff4809 100644 --- a/src/http/reply.cc +++ b/src/http/reply.cc @@ -44,13 +44,17 @@ module seastar; #include #endif +template<> +struct std::hash : public std::hash +{}; + namespace seastar { namespace http { namespace status_strings { -static const std::unordered_map status_strings = { +static const std::unordered_map status_strings = { {reply::status_type::continue_, "100 Continue"}, {reply::status_type::switching_protocols, "101 Switching Protocols"}, {reply::status_type::ok, "200 OK"}, @@ -100,7 +104,7 @@ static const std::unordered_map status_str {reply::status_type::network_connect_timeout, "599 Network Connect Timeout"}}; template -static auto with_string_view(reply::status_type status, Func&& func) -> std::invoke_result_t { +static auto with_string_view(status_type status, Func&& func) -> std::invoke_result_t { if (auto found = status_strings.find(status); found != status_strings.end()) [[likely]] { return func(found->second); } @@ -110,12 +114,16 @@ static auto with_string_view(reply::status_type status, Func&& func) -> std::inv } // namespace status_strings -std::ostream& operator<<(std::ostream& os, reply::status_type st) { +std::ostream& operator<<(std::ostream& os, status_type st) { return status_strings::with_string_view(st, [&](std::string_view txt) -> std::ostream& { return os << txt; }); } +std::ostream& operator<<(std::ostream& os, status_type::status_init st) { + return os << status_type(st); +} + sstring reply::response_line() const { return status_strings::with_string_view(_status, [this](std::string_view txt) { return seastar::format("HTTP/{} {}\r\n", _version, txt); From ff9a83c82e45a9f3c647731483b798c9e5aee634 Mon Sep 17 00:00:00 2001 From: Calle Wilund Date: Mon, 28 Jul 2025 14:14:39 +0000 Subject: [PATCH 2/3] http::status_type: Make it possible to bind lexical names to custom status values Adds a dllink time bind function for adding custom constants to the http::status_type known names. The purpose is to allow both automatic nice formatting of received replies, but also ensure that a reply _sent_ using a custom status would format equivalently to pre-defined codes. --- include/seastar/http/reply.hh | 20 ++++++ src/http/reply.cc | 130 +++++++++++++++++++--------------- tests/unit/httpd_test.cc | 42 +++++++++++ 3 files changed, 136 insertions(+), 56 deletions(-) diff --git a/include/seastar/http/reply.hh b/include/seastar/http/reply.hh index 2760bf8caf5..6cf7979de58 100644 --- a/include/seastar/http/reply.hh +++ b/include/seastar/http/reply.hh @@ -331,6 +331,26 @@ private: std::ostream& operator<<(std::ostream& os, status_type st); std::ostream& operator<<(std::ostream& os, status_type::status_init st); +/** + * Binds a defined status value to a lexical name. + * Must be called at dlload time (i.e. as a static const declaraion + * on file level), as this modifies a structure that must be readonly + * during reactor runtime. + * + * Pattern: + * + * .hh file: + * constexpr seastar::http::status_type MY_ERROR(); + * + * .cc file + * + * static const auto init_my_error = seastar::http::bind_status_name(MY_ERROR, "My Error that is nice"); + * + * @return a view of the bound string name. + * + */ +std::string_view bind_status_name(status_type, std::string_view); + } // namespace http namespace httpd { diff --git a/src/http/reply.cc b/src/http/reply.cc index 2f410ff4809..618f52c57b6 100644 --- a/src/http/reply.cc +++ b/src/http/reply.cc @@ -42,6 +42,7 @@ module seastar; #include #include #include +#include #endif template<> @@ -54,69 +55,88 @@ namespace http { namespace status_strings { -static const std::unordered_map status_strings = { - {reply::status_type::continue_, "100 Continue"}, - {reply::status_type::switching_protocols, "101 Switching Protocols"}, - {reply::status_type::ok, "200 OK"}, - {reply::status_type::created, "201 Created"}, - {reply::status_type::accepted, "202 Accepted"}, - {reply::status_type::nonauthoritative_information, "203 Non-Authoritative Information"}, - {reply::status_type::no_content, "204 No Content"}, - {reply::status_type::reset_content, "205 Reset Content"}, - {reply::status_type::partial_content, "206 Partial Content"}, - {reply::status_type::multiple_choices, "300 Multiple Choices"}, - {reply::status_type::moved_permanently, "301 Moved Permanently"}, - {reply::status_type::moved_temporarily, "302 Moved Temporarily"}, - {reply::status_type::see_other, "303 See Other"}, - {reply::status_type::not_modified, "304 Not Modified"}, - {reply::status_type::use_proxy, "305 Use Proxy"}, - {reply::status_type::temporary_redirect, "307 Temporary Redirect"}, - {reply::status_type::permanent_redirect, "308 Permanent Redirect"}, - {reply::status_type::bad_request, "400 Bad Request"}, - {reply::status_type::unauthorized, "401 Unauthorized"}, - {reply::status_type::payment_required, "402 Payment Required"}, - {reply::status_type::forbidden, "403 Forbidden"}, - {reply::status_type::not_found, "404 Not Found"}, - {reply::status_type::method_not_allowed, "405 Method Not Allowed"}, - {reply::status_type::not_acceptable, "406 Not Acceptable"}, - {reply::status_type::request_timeout, "408 Request Timeout"}, - {reply::status_type::conflict, "409 Conflict"}, - {reply::status_type::gone, "410 Gone"}, - {reply::status_type::length_required, "411 Length Required"}, - {reply::status_type::payload_too_large, "413 Payload Too Large"}, - {reply::status_type::uri_too_long, "414 URI Too Long"}, - {reply::status_type::unsupported_media_type, "415 Unsupported Media Type"}, - {reply::status_type::expectation_failed, "417 Expectation Failed"}, - {reply::status_type::page_expired, "419 Page Expired"}, - {reply::status_type::unprocessable_entity, "422 Unprocessable Entity"}, - {reply::status_type::upgrade_required, "426 Upgrade Required"}, - {reply::status_type::too_many_requests, "429 Too Many Requests"}, - {reply::status_type::login_timeout, "440 Login Timeout"}, - {reply::status_type::internal_server_error, "500 Internal Server Error"}, - {reply::status_type::not_implemented, "501 Not Implemented"}, - {reply::status_type::bad_gateway, "502 Bad Gateway"}, - {reply::status_type::service_unavailable, "503 Service Unavailable"}, - {reply::status_type::gateway_timeout, "504 Gateway Timeout"}, - {reply::status_type::http_version_not_supported, "505 HTTP Version Not Supported"}, - {reply::status_type::insufficient_storage, "507 Insufficient Storage"}, - {reply::status_type::bandwidth_limit_exceeded, "509 Bandwidth Limit Exceeded"}, - {reply::status_type::network_read_timeout, "598 Network Read Timeout"}, - {reply::status_type::network_connect_timeout, "599 Network Connect Timeout"}}; +static auto& status_strings() { + static std::unordered_map status_strings = { + {reply::status_type::continue_, "Continue"}, + {reply::status_type::switching_protocols, "Switching Protocols"}, + {reply::status_type::ok, "OK"}, + {reply::status_type::created, "Created"}, + {reply::status_type::accepted, "Accepted"}, + {reply::status_type::nonauthoritative_information, "Non-Authoritative Information"}, + {reply::status_type::no_content, "No Content"}, + {reply::status_type::reset_content, "Reset Content"}, + {reply::status_type::partial_content, "Partial Content"}, + {reply::status_type::multiple_choices, "Multiple Choices"}, + {reply::status_type::moved_permanently, "Moved Permanently"}, + {reply::status_type::moved_temporarily, "Moved Temporarily"}, + {reply::status_type::see_other, "See Other"}, + {reply::status_type::not_modified, "Not Modified"}, + {reply::status_type::use_proxy, "Use Proxy"}, + {reply::status_type::temporary_redirect, "Temporary Redirect"}, + {reply::status_type::permanent_redirect, "Permanent Redirect"}, + {reply::status_type::bad_request, "Bad Request"}, + {reply::status_type::unauthorized, "Unauthorized"}, + {reply::status_type::payment_required, "Payment Required"}, + {reply::status_type::forbidden, "Forbidden"}, + {reply::status_type::not_found, "Not Found"}, + {reply::status_type::method_not_allowed, "Method Not Allowed"}, + {reply::status_type::not_acceptable, "Not Acceptable"}, + {reply::status_type::request_timeout, "Request Timeout"}, + {reply::status_type::conflict, "Conflict"}, + {reply::status_type::gone, "Gone"}, + {reply::status_type::length_required, "Length Required"}, + {reply::status_type::payload_too_large, "Payload Too Large"}, + {reply::status_type::uri_too_long, "URI Too Long"}, + {reply::status_type::unsupported_media_type, "Unsupported Media Type"}, + {reply::status_type::expectation_failed, "Expectation Failed"}, + {reply::status_type::page_expired, "Page Expired"}, + {reply::status_type::unprocessable_entity, "Unprocessable Entity"}, + {reply::status_type::upgrade_required, "Upgrade Required"}, + {reply::status_type::too_many_requests, "Too Many Requests"}, + {reply::status_type::login_timeout, "Login Timeout"}, + {reply::status_type::internal_server_error, "Internal Server Error"}, + {reply::status_type::not_implemented, "Not Implemented"}, + {reply::status_type::bad_gateway, "Bad Gateway"}, + {reply::status_type::service_unavailable, "Service Unavailable"}, + {reply::status_type::gateway_timeout, "Gateway Timeout"}, + {reply::status_type::http_version_not_supported, "HTTP Version Not Supported"}, + {reply::status_type::insufficient_storage, "Insufficient Storage"}, + {reply::status_type::bandwidth_limit_exceeded, "Bandwidth Limit Exceeded"}, + {reply::status_type::network_read_timeout, "Network Read Timeout"}, + {reply::status_type::network_connect_timeout, "Network Connect Timeout"}}; + + return status_strings; +} template static auto with_string_view(status_type status, Func&& func) -> std::invoke_result_t { - if (auto found = status_strings.find(status); found != status_strings.end()) [[likely]] { + const auto& ss = status_strings(); + if (auto found = ss.find(status); found != ss.end()) [[likely]] { return func(found->second); - } - auto dummy_buf = std::to_string(int(status)); - return func(dummy_buf); + } + auto dummy_buf = std::to_string(int(status)); + return func(dummy_buf); } } // namespace status_strings +std::string_view bind_status_name(status_type st, std::string_view name) { + if (engine_is_ready()) { + throw std::runtime_error("Cannot bind http status name in runtime"); + } + auto& map = status_strings::status_strings(); + auto p = map.try_emplace(st, name); + if (!p.second) { + throw std::invalid_argument(seastar::format("Status {} {} already bound. Previous name: {}", + int(st), name, p.first->second + )); + } + return p.first->second; +} + std::ostream& operator<<(std::ostream& os, status_type st) { return status_strings::with_string_view(st, [&](std::string_view txt) -> std::ostream& { - return os << txt; + return os << int(st) << " " << txt; }); } @@ -125,9 +145,7 @@ std::ostream& operator<<(std::ostream& os, status_type::status_init st) { } sstring reply::response_line() const { - return status_strings::with_string_view(_status, [this](std::string_view txt) { - return seastar::format("HTTP/{} {}\r\n", _version, txt); - }); + return seastar::format("HTTP/{} {}\r\n", _version, _status); } void reply::write_body(const sstring& content_type, noncopyable_function(output_stream&&)>&& body_writer) { diff --git a/tests/unit/httpd_test.cc b/tests/unit/httpd_test.cc index 1f279492429..d64d493fafd 100644 --- a/tests/unit/httpd_test.cc +++ b/tests/unit/httpd_test.cc @@ -2178,3 +2178,45 @@ SEASTAR_TEST_CASE(test_client_close_connection) { } }); } + +static constexpr http::status_type my_http_status(555); +static const auto my_http_status_name = http::bind_status_name(my_http_status, "My Super Error"); + +SEASTAR_TEST_CASE(test_formatting_custom_error) { + BOOST_REQUIRE_EQUAL(fmt::format("{} {}", int(my_http_status), my_http_status_name), fmt::format("{}", my_http_status)); + + try { + throw httpd::unexpected_status_error(my_http_status); + } catch (const std::exception& ex) { + BOOST_REQUIRE_EQUAL(sstring(ex.what()), format("{}", my_http_status)); + } + + http::reply r; + r.set_version("1.1"); + r.set_status(my_http_status); + + BOOST_REQUIRE_EQUAL( + fmt::format("HTTP/1.1 {} {}\r\n", int(my_http_status), my_http_status_name), + r.response_line() + ); + + // just checking that expected + // value switch expression works. + switch (r._status) { + case my_http_status: break; // ok + case http::status_type::ok: + default: + BOOST_FAIL("wrong value"); + } + + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_cannot_bind_status_name_in_runtime) { + constexpr http::status_type my_borked_http_status(556); + BOOST_REQUIRE_THROW( + http::bind_status_name(my_borked_http_status, "My Broken Error"), + std::runtime_error + ); + return make_ready_future<>(); +} From d6731749751152a1be56813051e611816737b4a2 Mon Sep 17 00:00:00 2001 From: Calle Wilund Date: Mon, 28 Jul 2025 14:28:53 +0000 Subject: [PATCH 3/3] http::status_type: Add some more constants Add a few missing standard (RFC 9110) error codes: 407 Proxy Authentication Required 412 Precondition Failed 416 Range Not Satisfiable 421 Misdirected Request Rename some string constant names to proper, modern names. Code 302 is renamed from moved_temporarily to found, but also aliased to the old constant name. String name is "Found". This is shamelessly adapted from a patch by @nyh, but without removing any codes (once using code adapts to custom constants this can be done perhaps). --- include/seastar/http/reply.hh | 11 +++++++++++ src/http/reply.cc | 14 ++++++++++---- tests/unit/httpd_test.cc | 4 ++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/include/seastar/http/reply.hh b/include/seastar/http/reply.hh index 6cf7979de58..bcb18ab87b6 100644 --- a/include/seastar/http/reply.hh +++ b/include/seastar/http/reply.hh @@ -94,6 +94,12 @@ struct status_type { } }; + // The following list of status codes is part of the HTTP standard, + // and are defined in RFC 9110, and in a few case in older RFCs as + // listed in IANA's "HTTP Status Code Registry". Please do not add + // to this list non-standard error codes. Seastar applications should + // be able to use non-standard error codes, but shouldn't expect + // Seastar to give them official names. static constexpr status_init continue_{100}; //!< continue static constexpr status_init switching_protocols{101}; //!< switching_protocols static constexpr status_init ok{200}; //!< ok @@ -106,6 +112,7 @@ struct status_type { static constexpr status_init multiple_choices{300}; //!< multiple_choices static constexpr status_init moved_permanently{301}; //!< moved_permanently static constexpr status_init moved_temporarily{302}; //!< moved_temporarily + static constexpr status_init found{moved_temporarily}; //!< found is modern name for moved_temporarily static constexpr status_init see_other{303}; //!< see_other static constexpr status_init not_modified{304}; //!< not_modified static constexpr status_init use_proxy{305}; //!< use_proxy @@ -118,15 +125,19 @@ struct status_type { static constexpr status_init not_found{404}; //!< not_found static constexpr status_init method_not_allowed{405}; //!< method_not_allowed static constexpr status_init not_acceptable{406}; //!< not_acceptable + static constexpr status_init proxy_authentication_required{407}; //