From d99c697be618e372325f18fa6e353f75fb23f357 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sun, 18 Oct 2026 18:40:23 -0400 Subject: [PATCH] Report a response body over the limit as ExceedMaxPayloadSize A buffered request whose response body went over set_payload_max_length(), also once decompressed, failed with Error::Read, while the Stream API reports the same limit as Error::ExceedMaxPayloadSize. read_content() already marks it with a 413 status, so map that status to ExceedMaxPayloadSize next to the 415 and 500 the client maps already. --- README.md | 2 ++ docs-src/pages/en/cookbook/c17-error-codes.md | 1 + docs-src/pages/ja/cookbook/c17-error-codes.md | 1 + httplib.h | 8 +++-- test/test.cc | 33 +++++++++++++++++++ 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dd6c5b4e..d8e3c163 100644 --- a/README.md +++ b/README.md @@ -1268,6 +1268,8 @@ cli.set_max_timeout(std::chrono::seconds(5)); cli.set_payload_max_length(1024 * 1024 * 512); // 512MB ``` +A response body over the limit, also once decompressed, fails the request with `Error::ExceedMaxPayloadSize`. + ### Receive content with a content receiver ```c++ diff --git a/docs-src/pages/en/cookbook/c17-error-codes.md b/docs-src/pages/en/cookbook/c17-error-codes.md index 407d0467..7e91d2e9 100644 --- a/docs-src/pages/en/cookbook/c17-error-codes.md +++ b/docs-src/pages/en/cookbook/c17-error-codes.md @@ -30,6 +30,7 @@ Use `if (res)` to check success. On failure, `res.error()` returns a `httplib::E | `Error::Connection` | Couldn't connect to the server, or the `Client` was built from a URL it can't use (userinfo or a backslash in the authority, a bad port). In the latter case `cli.is_valid()` is `false` | | `Error::ConnectionTimeout` | Connection timeout (`set_connection_timeout`) | | `Error::Read` / `Error::Write` | Error during send or receive. A timeout from `set_read_timeout` or `set_max_timeout` is also reported as `Error::Read` | +| `Error::ExceedMaxPayloadSize` | The response body is larger than `set_payload_max_length()` allows, also once decompressed | | `Error::Timeout` | A body read timed out in `stream::Get()` or `SSEClient` | | `Error::ExceedRedirectCount` | Too many redirects | | `Error::SSLConnection` | TLS handshake failed | diff --git a/docs-src/pages/ja/cookbook/c17-error-codes.md b/docs-src/pages/ja/cookbook/c17-error-codes.md index adfb8413..5f442fcc 100644 --- a/docs-src/pages/ja/cookbook/c17-error-codes.md +++ b/docs-src/pages/ja/cookbook/c17-error-codes.md @@ -30,6 +30,7 @@ if (res) { | `Error::Connection` | サーバーに接続できなかった。または、使えないURL(authorityにuserinfoやバックスラッシュがある、ポートが不正など)から`Client`を作った。後者の場合は`cli.is_valid()`が`false`になる | | `Error::ConnectionTimeout` | 接続タイムアウト(`set_connection_timeout`) | | `Error::Read` / `Error::Write` | 送受信中のエラー。`set_read_timeout`や`set_max_timeout`によるタイムアウトも`Error::Read`になる | +| `Error::ExceedMaxPayloadSize` | レスポンスボディが`set_payload_max_length()`の上限を超えた(解凍後に超えた場合も含む) | | `Error::Timeout` | `stream::Get()`や`SSEClient`で、ボディの読み取りがタイムアウトした | | `Error::ExceedRedirectCount` | リダイレクト回数が上限を超えた | | `Error::SSLConnection` | TLSハンドシェイクに失敗 | diff --git a/httplib.h b/httplib.h index c2d46ff2..4958ce0f 100644 --- a/httplib.h +++ b/httplib.h @@ -17019,9 +17019,13 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, std::move(progress), std::move(out), decompress_)) { if (error != Error::Canceled) { - // Tell the caller apart from a plain read failure when the body could - // not be decoded because of its Content-Encoding. + // Tell the caller apart from a plain read failure when the body was + // over the payload limit or could not be decoded because of its + // Content-Encoding. switch (content_status) { + case StatusCode::PayloadTooLarge_413: + error = Error::ExceedMaxPayloadSize; + break; case StatusCode::UnsupportedMediaType_415: error = Error::UnsupportedContentEncoding; break; diff --git a/test/test.cc b/test/test.cc index d6e495e4..7adb3024 100644 --- a/test/test.cc +++ b/test/test.cc @@ -14389,6 +14389,39 @@ TEST(ClientPayloadTest, DoesNotReserveTheClaimedLength) { EXPECT_FALSE(cli.Get("/")); } +// A response body over the limit is reported as ExceedMaxPayloadSize, as the +// Stream API does, not as a plain read failure +TEST(ClientPayloadTest, OverLimitIsExceedMaxPayloadSize) { + Server svr; + svr.Get("/big", [](const Request &, Response &res) { + res.set_content(std::string(100000, 'b'), "text/plain"); + }); + + auto port = svr.bind_to_any_port(HOST); + auto thread = std::thread([&]() { svr.listen_after_bind(); }); + auto se = detail::scope_exit([&] { + svr.stop(); + thread.join(); + }); + svr.wait_until_ready(); + + Client cli(HOST, port); + cli.set_payload_max_length(1000); + + auto res = cli.Get("/big", Headers{{"Accept-Encoding", ""}}); + EXPECT_EQ(Error::ExceedMaxPayloadSize, res.error()); + + res = cli.Get("/big", Headers{{"Accept-Encoding", ""}}, + [](const char *, size_t) { return true; }); + EXPECT_EQ(Error::ExceedMaxPayloadSize, res.error()); + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + // Small on the wire, over the limit once decompressed + res = cli.Get("/big", Headers{{"Accept-Encoding", "gzip"}}); + EXPECT_EQ(Error::ExceedMaxPayloadSize, res.error()); +#endif +} + #ifdef CPPHTTPLIB_ZLIB_SUPPORT // `payload_max_length` is not enforced on decompressed body in ContentReader // path.