Compare commits

...
6 Commits
Author SHA1 Message Date
yhirose 7e5d0683ac Release v0.65.0 2026-11-05 07:39:47 -05:00
yhirose 6df971434e Do not carry Request::params over to a redirect's Location
A request sent with send(Request) and params got those params appended
to the Location of a redirect that had no query, on the same origin or
another one. The Location alone names the next target, as it already
did for Get/Post(path, params).
2026-11-05 07:04:31 -05:00
Eminy 39c98d4671 Ignore a Range header with a unit other than bytes (#2657)
RFC 9110 14.2 says an origin server MUST ignore a Range header field
with a range unit it does not understand. parse_range_header() accepts
only bytes=, so a request such as "Range: items=1-2" got a 416 instead
of the full response. The unit is now checked first, and only a bytes
range is parsed and answered with a 416 when it is invalid.
2026-11-04 22:44:22 -05:00
Wei-Lun Liandyhirose a1ba73a256 Cap a send() or wolfSSL_read() length at INT_MAX (#2655)
* Cap a send() or wolfSSL_read() length at INT_MAX

On 64-bit Windows SocketStream::write() handed size to send(), whose
length is an int, so a single write of 4 GiB or more went out as its low
32 bits: a 4 GiB + 1 byte write sent one byte, and write_data() then
retried the remaining 4 GiB as zero-byte sends forever. The INT_MAX cap
that read() has was limited to 32-bit Windows in df20c27 (#1294), when
the body was still written with one unchecked write(); write_data() now
sends the rest, so the cap applies to every Windows build again.

The wolfSSL tls::read() had the same cast, so a read into a buffer over
INT_MAX failed. Cap it as the OpenSSL backend has done since f80864c.

* Drop the 4 GiB write test and shorten comments

The Windows-only test allocated 4 GiB and took about 5 s, which is too
heavy for CI.

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-11-04 18:06:30 -05:00
metsw24-maxandyhirose 453b1b8613 keep a relative Location on its host in resolve_relative_location (#2654)
* keep a relative Location on its host in resolve_relative_location

* Shorten the comment on on_this_host in resolve_relative_location

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-11-02 13:12:31 -05:00
Yoshiki Matsuda a18895f42e Let ClientImpl::stop() interrupt a request waiting on Windows (#2653)
stop() shuts the socket down to end a request in flight, but on Windows
that does not wake a read waiting in WSAPoll (#1396). The request then
waited until the server answered, the read timeout passed, or the OS
dropped the connection.

stop() now also sets a flag, and send_() gives the request's stream an
interrupt check that reads it, as SSEClient::stop() does since 801bf2e.
The wait for data then ends within 100 ms on every platform. A request
started after stop() clears the flag. A redirect or a digest retry sent
inside the request keeps it.

select_read_interruptible() now rounds its last wait up to a whole
millisecond. poll() takes milliseconds, and the wait ended up to 1 ms
before the deadline, which MaxTimeoutTest caught once every client
request waited through this function.
2026-10-30 13:40:50 -04:00
3 changed files with 277 additions and 13 deletions

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.64.0"
version = "0.65.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+44 -12
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.64.0"
#define CPPHTTPLIB_VERSION_NUM "0x004000"
#define CPPHTTPLIB_VERSION "0.65.0"
#define CPPHTTPLIB_VERSION_NUM "0x004100"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -1029,18 +1029,27 @@ inline std::string resolve_relative_location(const std::string &location,
// A ':' in the first segment means the value has a scheme.
if (location.find(':') < location.find_first_of("/?#")) { return location; }
if (location[0] == '#') { return base.substr(0, base.find('#')) + location; }
// A path starting with "//" would be re-parsed as an authority. Prefix "/."
// (as the URL Standard does); remove_dot_segments strips it later.
auto on_this_host = [](std::string path) -> std::string {
if (path.compare(0, 2, "//") == 0) { path.insert(0, "/."); }
return path;
};
if (location[0] == '#') {
return on_this_host(base.substr(0, base.find('#')) + location);
}
auto base_path = base.substr(0, base.find_first_of("?#"));
if (location[0] == '?') { return base_path + location; }
if (location[0] == '?') { return on_this_host(base_path + location); }
if (base_path.empty() || base_path[0] != '/') { base_path = "/"; }
auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location;
// Remove "." and ".." segments from the merged path.
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
return remove_dot_segments(merged.substr(0, path_end)) +
merged.substr(path_end);
return on_this_host(remove_dot_segments(merged.substr(0, path_end)) +
merged.substr(path_end));
}
} // namespace detail
@@ -3058,6 +3067,10 @@ protected:
bool socket_should_be_closed_when_request_is_done_ = false;
size_t socket_tls_settings_version_ = 0;
// Set by stop() while a request is in flight. The request's reads poll it,
// because shutting the socket down does not wake a read on Windows.
std::atomic<bool> stop_requested_{false};
std::atomic<size_t> tls_settings_version_{0};
// Hostname to connection target map. The value is an IP literal or another
@@ -6840,10 +6853,14 @@ select_read_interruptible(socket_t sock, time_t sec, time_t usec,
while (!interrupted()) {
auto left = std::chrono::duration_cast<std::chrono::microseconds>(
deadline - std::chrono::steady_clock::now());
auto wait =
(std::max)((std::min)(left, slice), std::chrono::microseconds(0));
auto ret = select_impl(sock, POLLIN, 0, static_cast<time_t>(wait.count()));
if (ret != 0 || wait < slice) { return ret; }
auto last = left < slice;
// poll() takes whole milliseconds, so round the last wait up to keep it
// from ending before the deadline.
auto wait_usec = last ? (std::max)((left.count() + 999) / 1000 * 1000,
static_cast<decltype(left.count())>(0))
: slice.count();
auto ret = select_impl(sock, POLLIN, 0, static_cast<time_t>(wait_usec));
if (ret != 0 || last) { return ret; }
}
return 0;
}
@@ -9335,6 +9352,7 @@ inline bool redirect(T &cli, Request &req, Response &res,
Error &error) {
Request new_req = req;
new_req.path = path;
new_req.params.clear();
new_req.redirect_count_ -= 1;
if (res.status == StatusCode::SeeOther_303 &&
@@ -12859,7 +12877,8 @@ inline ssize_t SocketStream::read(char *ptr, size_t size) {
inline ssize_t SocketStream::write(const char *ptr, size_t size) {
if (!wait_writable()) { return -1; }
#if defined(_WIN32) && !defined(_WIN64)
// Winsock's send() takes an int length, even on 64-bit Windows
#ifdef _WIN32
size =
(std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
#endif
@@ -15274,7 +15293,10 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
if ((req.method == "GET" || req.method == "HEAD") &&
req.has_header("Range")) {
const auto &range_header_value = req.get_header_value("Range");
if (!detail::parse_range_header(range_header_value, req.ranges)) {
// RFC 9110 14.2: a range unit other than bytes must be ignored
auto unit = range_header_value.substr(0, range_header_value.find('='));
if (detail::case_ignore::equal(unit, "bytes") &&
!detail::parse_range_header(range_header_value, req.ranges)) {
connection_closed = true;
res.status = StatusCode::RangeNotSatisfiable_416;
output_error_log(Error::InvalidRangeHeader, &req);
@@ -15803,6 +15825,9 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
// socket.
socket_should_be_closed_when_request_is_done_ = false;
// A redirect or a retry sent inside a request keeps its stop()
if (socket_requests_in_flight_ == 0) { stop_requested_ = false; }
if (!socket_reusable()) {
if (!ensure_socket_connection(socket_, error)) { return false; }
@@ -15850,6 +15875,7 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
});
ret = process_socket(socket_, req.start_time_, [&](Stream &strm) {
strm.set_read_interrupt([this] { return stop_requested_.load(); });
return handle_request(strm, req, res, close_connection, error);
});
@@ -18068,6 +18094,9 @@ inline void ClientImpl::stop() {
// are not thread-safe.
if (socket_requests_in_flight_ > 0) {
shutdown_socket(socket_);
// shutdown() does not wake a read waiting in WSAPoll on Windows, so the
// request's reads also give up once they see this flag.
stop_requested_ = true;
// Aside from that, we set a flag for the socket to be closed when we're
// done.
@@ -23030,6 +23059,9 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
}
auto wsession = static_cast<impl::WolfSSLSession *>(session);
constexpr auto max_len =
static_cast<size_t>((std::numeric_limits<int>::max)());
if (len > max_len) { len = max_len; }
int ret = wolfSSL_read(wsession->ssl, buf, static_cast<int>(len));
if (ret > 0) {
+232
View File
@@ -2818,6 +2818,35 @@ TEST(RangeTest, IgnoredForPost) {
}
}
TEST(RangeTest, IgnoredForUnknownUnit) {
Server svr;
svr.Get("/", [](const Request &, Response &res) {
res.set_content("abcdefg", "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
ASSERT_GT(port, 0);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
for (auto range : {"items=1-2", "none=x"}) {
auto res = cli.Get("/", Headers{{"Range", range}});
ASSERT_TRUE(res) << range << ": " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status) << range;
EXPECT_EQ("abcdefg", res->body) << range;
}
auto res = cli.Get("/", Headers{{"Range", "bytes=x"}});
ASSERT_TRUE(res) << to_string(res.error());
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
}
TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
auto host = "unresolvableaddress.local";
auto path = std::string{"/"};
@@ -3445,6 +3474,55 @@ TEST(CancelTest, WithCancelLargePayloadDelete) {
EXPECT_EQ(Error::Canceled, res.error());
}
// One stop() from another thread ends a request that waits for the response
TEST(ClientStopTest, StopEndsWaitForResponse) {
Server svr;
std::atomic<bool> received{false};
std::atomic<bool> done{false};
svr.Get("/wait", [&](const Request & /*req*/, Response &res) {
received.store(true);
while (!done.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
res.set_content("Hello World!", "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
auto thread = std::thread([&]() { svr.listen_after_bind(); });
Client cli(HOST, port);
cli.set_read_timeout(std::chrono::seconds(30));
std::future<Result> request;
auto se = detail::scope_exit([&] {
done.store(true);
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
request = std::async(std::launch::async, [&]() { return cli.Get("/wait"); });
for (int i = 0; i < 500 && !received.load(); ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
ASSERT_TRUE(received.load());
// Let the client block in the wait for the response
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto start = std::chrono::steady_clock::now();
cli.stop();
ASSERT_EQ(std::future_status::ready,
request.wait_for(std::chrono::seconds(5)));
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds(2));
auto res = request.get();
ASSERT_TRUE(!res);
EXPECT_EQ(Error::Read, res.error());
}
static std::string remove_whitespace(const std::string &input) {
std::string output;
output.reserve(input.size());
@@ -4248,6 +4326,62 @@ TEST(RedirectToSamePort, ForwardCookie) {
EXPECT_EQ("session_id=SECRET", captured_cookie);
}
TEST(RedirectTest, DoNotCarryRequestParamsToLocation) {
// The Location alone names the next target, on any origin.
Server target_svr;
std::string cross_target;
target_svr.Get("/landing", [&](const Request &req, Response &res) {
cross_target = req.target;
res.set_content("OK", "text/plain");
});
auto target_port = target_svr.bind_to_any_port(HOST);
auto target_thread = std::thread([&]() { target_svr.listen_after_bind(); });
Server origin_svr;
std::string same_target;
origin_svr.Get("/cross", [&](const Request &, Response &res) {
res.set_redirect(
"http://localhost:" + std::to_string(target_port) + "/landing", 302);
});
origin_svr.Get("/same", [&](const Request &, Response &res) {
res.set_redirect("/landing", 302);
});
origin_svr.Get("/landing", [&](const Request &req, Response &res) {
same_target = req.target;
res.set_content("OK", "text/plain");
});
auto origin_port = origin_svr.bind_to_any_port(HOST);
auto origin_thread = std::thread([&]() { origin_svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
origin_svr.stop();
origin_thread.join();
target_svr.stop();
target_thread.join();
});
target_svr.wait_until_ready();
origin_svr.wait_until_ready();
Client cli("localhost", origin_port);
cli.set_follow_location(true);
Request req;
req.method = "GET";
req.params = {{"api_key", "SECRET"}};
req.path = "/cross";
auto res = cli.send(req);
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("/landing", cross_target);
req.path = "/same";
res = cli.send(req);
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("/landing", same_target);
}
TEST(RedirectToDifferentPort, ForwardDefaultHeadersButNotCredentials) {
std::string authorization;
std::string trace_id;
@@ -20676,6 +20810,13 @@ TEST(RedirectTest, ResolveRelativeLocation) {
detail::resolve_relative_location(c.first, "/b/c/d;p?q"))
<< c.first;
}
// A result starting with "//" is still a path, not a network-path reference
EXPECT_EQ("/.//g",
detail::resolve_relative_location("../../..//g", "/b/c/d;p?q"));
EXPECT_EQ("/.//g/x", detail::resolve_relative_location("x", "//g/h"));
EXPECT_EQ("/.//g/h?y", detail::resolve_relative_location("?y", "//g/h"));
EXPECT_EQ("/.//g/h#s", detail::resolve_relative_location("#s", "//g/h"));
}
TEST(RedirectTest, RemoveDotSegments) {
@@ -20763,6 +20904,54 @@ TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) {
}
}
// "..//host:port/x" resolves to the path "//host:port/x" on the same host
// (RFC 3986 5.2), as curl and the URL Standard have it, not to another host.
TEST(RedirectTest, RelativeLocationStaysOnHost) {
Server other;
std::atomic<int> other_hits{0};
other.Get(".*", [&](const Request & /*req*/, Response &res) {
other_hits++;
res.set_content("other", "text/plain");
});
auto other_port = other.bind_to_any_port("127.0.0.1");
auto other_thread = std::thread([&]() { other.listen_after_bind(); });
Server svr;
svr.Get("/dir/page", [](const Request &req, Response &res) {
res.set_redirect(req.get_param_value("to"));
});
svr.Get(".*", [](const Request &req, Response &res) {
res.set_content(req.path, "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();
other.stop();
other_thread.join();
ASSERT_FALSE(svr.is_running());
ASSERT_FALSE(other.is_running());
});
svr.wait_until_ready();
other.wait_until_ready();
auto authority = "127.0.0.1:" + std::to_string(other_port);
for (const auto &to :
{"..//" + authority + "/x", "../..//" + authority + "/x"}) {
Client cli(HOST, port);
cli.set_follow_location(true);
auto res = cli.Get("/dir/page?to=" + encode_query_component(to));
ASSERT_TRUE(res) << to << ": " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status) << to;
EXPECT_EQ("//" + authority + "/x", res->body) << to;
}
EXPECT_EQ(0, other_hits.load());
}
#ifdef CPPHTTPLIB_SSL_ENABLED
TEST(RedirectTest, Issue2185_Online) {
SSLClient client("github.com");
@@ -24952,6 +25141,49 @@ TEST(SSLTest, ResponseBodyTerminatedByConnectionClose) {
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(expected_body, res->body);
}
// Without Content-Length, the full buffer size reaches tls::read().
TEST(SSLTest, ReadIntoBufferOverIntMax) {
if (sizeof(size_t) <= sizeof(int)) {
GTEST_SKIP() << "size_t cannot exceed INT_MAX here";
}
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
svr.Get("/no-content-length", [&](const Request & /*req*/, Response &res) {
res.set_content_provider("text/plain",
[&](size_t offset, DataSink &sink) -> bool {
if (offset == 0) { sink.write("Hello", 5); }
sink.done();
return true;
});
});
auto port = svr.bind_to_any_port(HOST);
auto listen_thread = std::thread([&svr] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
});
svr.wait_until_ready();
SSLClient cli(HOST, port);
cli.enable_server_certificate_verification(false);
auto handle = cli.open_stream("GET", "/no-content-length");
ASSERT_TRUE(handle.is_valid()) << to_string(handle.error);
const auto len = static_cast<size_t>((std::numeric_limits<int>::max)()) + 1;
std::unique_ptr<char[]> buf(new char[len]);
std::string body;
ssize_t n;
while ((n = handle.read(buf.get(), len)) > 0) {
body.append(buf.get(), static_cast<size_t>(n));
}
EXPECT_EQ(0, n);
EXPECT_EQ("Hello", body);
}
#endif // CPPHTTPLIB_SSL_ENABLED
//==============================================================================