Compare commits

..

13 Commits

Author SHA1 Message Date
yhirose b6c55c6030 Release v0.22.0 2025-06-24 08:01:53 -04:00
yhirose 28dcf379e8 Merge commit from fork 2025-06-24 07:56:00 -04:00
yhirose 91e79e9a63 Fix #1777 (#2160)
* Add benchmark unit test

* Update

* Update

* Update

* Change the default value of CPPHTTPLIB_IDLE_INTERVAL_USECOND to 1ms
2025-06-24 07:44:10 -04:00
yhirose 27879c4874 Fix #2157 (#2158)
* Fix #2157

* Fix Windows build error: wrap std::max in parentheses to avoid macro conflict

- On Windows, max/min are often defined as macros by windows.h
- This causes compilation errors with std::max/std::min
- Solution: use (std::max) to prevent macro expansion
- Fixes CI build error: error C2589: '(': illegal token on right side of '::'

Fixes: error in coalesce_ranges function on line 5376
2025-06-23 08:35:56 -04:00
DSiekmeier 08a0452fb2 Update README.md (#2156)
According to the curl manpage, the option is called "--max-time".
2025-06-18 07:05:23 -04:00
yhirose 3a1f379e75 Release v0.21.0 2025-06-09 22:19:30 -04:00
Marek Kwasecki fd8da4d8e4 Feature/multipart headers (#2152)
* Adds headers to multipart form data

Adds a `headers` field to the `MultipartFormData` struct.

Populates this field by parsing headers from the multipart form data.
This allows access to specific headers associated with each form data part.

* Adds multipart header access test

Verifies the correct retrieval of headers from multipart form data file parts.

Ensures that custom and content-related headers are accessible and parsed as expected.

* Enables automatic test discovery with GoogleTest

Uses `gtest_discover_tests` to automatically find and run tests, simplifying test maintenance and improving discoverability.

* Removes explicit GoogleTest include

* Refactors header parsing logic

Improves header parsing by using a dedicated parsing function,
resulting in cleaner and more robust code.

This change also adds error handling during header parsing,
returning an error and marking the request as invalid
if parsing fails.

* clang-format corrected

* Renames variable for better readability.

Renames the `customHeader` variable to `custom_header` for improved code readability and consistency.

* typo
2025-06-09 15:59:25 -04:00
yhirose 365cbe37fa Fix #2101 2025-05-25 21:56:28 -04:00
Piotr 4a7aae5469 Detect if afunix.h exists (#2145)
Signed-off-by: Piotr Stankiewicz <piotr.stankiewicz@docker.com>
2025-05-23 23:14:48 -04:00
yhirose fd324e1412 Add KeepAliveTest.MaxCount 2025-05-20 21:41:50 +09:00
QWERTIOX 366eda72dc Specify version in meson.build (#2139)
I prefer to specify version when I'm calling dependency() and lack of makes me unable to do it
2025-05-16 10:02:32 -04:00
yhirose c216dc94d2 Code cleanup 2025-05-09 18:45:31 +09:00
yhirose 61893a00a4 Fix #2135 2025-05-03 22:50:47 +09:00
4 changed files with 508 additions and 36 deletions
+17 -1
View File
@@ -285,6 +285,22 @@ svr.set_post_routing_handler([](const auto& req, auto& res) {
});
```
### Pre request handler
```cpp
svr.set_pre_request_handler([](const auto& req, auto& res) {
if (req.matched_route == "/user/:user") {
auto user = req.path_params.at("user");
if (user != "john") {
res.status = StatusCode::Forbidden_403;
res.set_content("error", "text/html");
return Server::HandlerResponse::Handled;
}
}
return Server::HandlerResponse::Unhandled;
});
```
### 'multipart/form-data' POST data
```cpp
@@ -655,7 +671,7 @@ cli.set_connection_timeout(0, 300000); // 300 milliseconds
cli.set_read_timeout(5, 0); // 5 seconds
cli.set_write_timeout(5, 0); // 5 seconds
// This method works the same as curl's `--max-timeout` option
// This method works the same as curl's `--max-time` option
svr.set_max_timeout(5000); // 5 seconds
```
+144 -25
View File
@@ -8,7 +8,7 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.20.1"
#define CPPHTTPLIB_VERSION "0.22.0"
/*
* Configuration
@@ -76,7 +76,7 @@
#ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND
#ifdef _WIN32
#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 10000
#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 1000
#else
#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0
#endif
@@ -90,6 +90,10 @@
#define CPPHTTPLIB_HEADER_MAX_LENGTH 8192
#endif
#ifndef CPPHTTPLIB_HEADER_MAX_COUNT
#define CPPHTTPLIB_HEADER_MAX_COUNT 100
#endif
#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT
#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20
#endif
@@ -192,8 +196,13 @@ using ssize_t = long;
#include <winsock2.h>
#include <ws2tcpip.h>
#if defined(__has_include)
#if __has_include(<afunix.h>)
// afunix.h uses types declared in winsock2.h, so has to be included after it.
#include <afunix.h>
#define CPPHTTPLIB_HAVE_AFUNIX_H 1
#endif
#endif
#ifndef WSA_FLAG_NO_HANDLE_INHERIT
#define WSA_FLAG_NO_HANDLE_INHERIT 0x80
@@ -539,6 +548,7 @@ struct MultipartFormData {
std::string content;
std::string filename;
std::string content_type;
Headers headers;
};
using MultipartFormDataItems = std::vector<MultipartFormData>;
using MultipartFormDataMap = std::multimap<std::string, MultipartFormData>;
@@ -631,6 +641,7 @@ using Ranges = std::vector<Range>;
struct Request {
std::string method;
std::string path;
std::string matched_route;
Params params;
Headers headers;
std::string body;
@@ -882,10 +893,16 @@ namespace detail {
class MatcherBase {
public:
MatcherBase(std::string pattern) : pattern_(pattern) {}
virtual ~MatcherBase() = default;
const std::string &pattern() const { return pattern_; }
// Match request path and populate its matches and
virtual bool match(Request &request) const = 0;
private:
std::string pattern_;
};
/**
@@ -937,7 +954,8 @@ private:
*/
class RegexMatcher final : public MatcherBase {
public:
RegexMatcher(const std::string &pattern) : regex_(pattern) {}
RegexMatcher(const std::string &pattern)
: MatcherBase(pattern), regex_(pattern) {}
bool match(Request &request) const override;
@@ -1004,9 +1022,12 @@ public:
}
Server &set_exception_handler(ExceptionHandler handler);
Server &set_pre_routing_handler(HandlerWithResponse handler);
Server &set_post_routing_handler(Handler handler);
Server &set_pre_request_handler(HandlerWithResponse handler);
Server &set_expect_100_continue_handler(Expect100ContinueHandler handler);
Server &set_logger(Logger logger);
@@ -1087,8 +1108,7 @@ private:
bool listen_internal();
bool routing(Request &req, Response &res, Stream &strm);
bool handle_file_request(const Request &req, Response &res,
bool head = false);
bool handle_file_request(const Request &req, Response &res);
bool dispatch_request(Request &req, Response &res,
const Handlers &handlers) const;
bool dispatch_request_for_content_reader(
@@ -1149,6 +1169,7 @@ private:
ExceptionHandler exception_handler_;
HandlerWithResponse pre_routing_handler_;
Handler post_routing_handler_;
HandlerWithResponse pre_request_handler_;
Expect100ContinueHandler expect_100_continue_handler_;
Logger logger_;
@@ -2066,7 +2087,9 @@ template <size_t N> inline constexpr size_t str_len(const char (&)[N]) {
}
inline bool is_numeric(const std::string &str) {
return !str.empty() && std::all_of(str.begin(), str.end(), ::isdigit);
return !str.empty() &&
std::all_of(str.cbegin(), str.cend(),
[](unsigned char c) { return std::isdigit(c); });
}
inline uint64_t get_header_value_u64(const Headers &headers,
@@ -3550,6 +3573,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port,
hints.ai_flags = socket_flags;
}
#if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H)
if (hints.ai_family == AF_UNIX) {
const auto addrlen = host.length();
if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; }
@@ -3594,6 +3618,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port,
}
return sock;
}
#endif
auto service = std::to_string(port);
@@ -4334,6 +4359,8 @@ inline bool read_headers(Stream &strm, Headers &headers) {
char buf[bufsiz];
stream_line_reader line_reader(strm, buf, bufsiz);
size_t header_count = 0;
for (;;) {
if (!line_reader.getline()) { return false; }
@@ -4354,6 +4381,9 @@ inline bool read_headers(Stream &strm, Headers &headers) {
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
// Check header count limit
if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
// Exclude line terminator
auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
@@ -4363,6 +4393,8 @@ inline bool read_headers(Stream &strm, Headers &headers) {
})) {
return false;
}
header_count++;
}
return true;
@@ -4465,9 +4497,13 @@ inline bool read_content_chunked(Stream &strm, T &x,
// chunked transfer coding data without the final CRLF.
if (!line_reader.getline()) { return true; }
size_t trailer_header_count = 0;
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
// Check trailer header count limit
if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
// Exclude line terminator
constexpr auto line_terminator_len = 2;
auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
@@ -4477,6 +4513,8 @@ inline bool read_content_chunked(Stream &strm, T &x,
x.headers.emplace(key, val);
});
trailer_header_count++;
if (!line_reader.getline()) { return false; }
}
@@ -5025,6 +5063,16 @@ public:
return false;
}
// parse and emplace space trimmed headers into a map
if (!parse_header(
header.data(), header.data() + header.size(),
[&](const std::string &key, const std::string &val) {
file_.headers.emplace(key, val);
})) {
is_valid_ = false;
return false;
}
constexpr const char header_content_type[] = "Content-Type:";
if (start_with_case_ignore(header, header_content_type)) {
@@ -5124,6 +5172,7 @@ private:
file_.name.clear();
file_.filename.clear();
file_.content_type.clear();
file_.headers.clear();
}
bool start_with_case_ignore(const std::string &a, const char *b) const {
@@ -5297,13 +5346,68 @@ serialize_multipart_formdata(const MultipartFormDataItems &items,
return body;
}
inline void coalesce_ranges(Ranges &ranges, size_t content_length) {
if (ranges.size() <= 1) return;
// Sort ranges by start position
std::sort(ranges.begin(), ranges.end(),
[](const Range &a, const Range &b) { return a.first < b.first; });
Ranges coalesced;
coalesced.reserve(ranges.size());
for (auto &r : ranges) {
auto first_pos = r.first;
auto last_pos = r.second;
// Handle special cases like in range_error
if (first_pos == -1 && last_pos == -1) {
first_pos = 0;
last_pos = static_cast<ssize_t>(content_length);
}
if (first_pos == -1) {
first_pos = static_cast<ssize_t>(content_length) - last_pos;
last_pos = static_cast<ssize_t>(content_length) - 1;
}
if (last_pos == -1 || last_pos >= static_cast<ssize_t>(content_length)) {
last_pos = static_cast<ssize_t>(content_length) - 1;
}
// Skip invalid ranges
if (!(0 <= first_pos && first_pos <= last_pos &&
last_pos < static_cast<ssize_t>(content_length))) {
continue;
}
// Coalesce with previous range if overlapping or adjacent (but not
// identical)
if (!coalesced.empty()) {
auto &prev = coalesced.back();
// Check if current range overlaps or is adjacent to previous range
// but don't coalesce identical ranges (allow duplicates)
if (first_pos <= prev.second + 1 &&
!(first_pos == prev.first && last_pos == prev.second)) {
// Extend the previous range
prev.second = (std::max)(prev.second, last_pos);
continue;
}
}
// Add new range
coalesced.emplace_back(first_pos, last_pos);
}
ranges = std::move(coalesced);
}
inline bool range_error(Request &req, Response &res) {
if (!req.ranges.empty() && 200 <= res.status && res.status < 300) {
ssize_t content_len = static_cast<ssize_t>(
res.content_length_ ? res.content_length_ : res.body.size());
ssize_t prev_first_pos = -1;
ssize_t prev_last_pos = -1;
std::vector<std::pair<ssize_t, ssize_t>> processed_ranges;
size_t overwrapping_count = 0;
// NOTE: The following Range check is based on '14.2. Range' in RFC 9110
@@ -5346,18 +5450,21 @@ inline bool range_error(Request &req, Response &res) {
return true;
}
// Ranges must be in ascending order
if (first_pos <= prev_first_pos) { return true; }
// Request must not have more than two overlapping ranges
if (first_pos <= prev_last_pos) {
overwrapping_count++;
if (overwrapping_count > 2) { return true; }
for (const auto &processed_range : processed_ranges) {
if (!(last_pos < processed_range.first ||
first_pos > processed_range.second)) {
overwrapping_count++;
if (overwrapping_count > 2) { return true; }
break; // Only count once per range
}
}
prev_first_pos = (std::max)(prev_first_pos, first_pos);
prev_last_pos = (std::max)(prev_last_pos, last_pos);
processed_ranges.emplace_back(first_pos, last_pos);
}
// After validation, coalesce overlapping ranges as per RFC 9110
coalesce_ranges(req.ranges, static_cast<size_t>(content_len));
}
return false;
@@ -6216,7 +6323,8 @@ inline time_t BufferStream::duration() const { return 0; }
inline const std::string &BufferStream::get_buffer() const { return buffer; }
inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) {
inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern)
: MatcherBase(pattern) {
constexpr const char marker[] = "/:";
// One past the last ending position of a path param substring
@@ -6467,6 +6575,11 @@ inline Server &Server::set_post_routing_handler(Handler handler) {
return *this;
}
inline Server &Server::set_pre_request_handler(HandlerWithResponse handler) {
pre_request_handler_ = std::move(handler);
return *this;
}
inline Server &Server::set_logger(Logger logger) {
logger_ = std::move(logger);
return *this;
@@ -6878,8 +6991,7 @@ Server::read_content_core(Stream &strm, Request &req, Response &res,
return true;
}
inline bool Server::handle_file_request(const Request &req, Response &res,
bool head) {
inline bool Server::handle_file_request(const Request &req, Response &res) {
for (const auto &entry : base_dirs_) {
// Prefix match
if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) {
@@ -6912,7 +7024,7 @@ inline bool Server::handle_file_request(const Request &req, Response &res,
return true;
});
if (!head && file_request_handler_) {
if (req.method != "HEAD" && file_request_handler_) {
file_request_handler_(req, res);
}
@@ -7046,9 +7158,8 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) {
}
// File handler
auto is_head_request = req.method == "HEAD";
if ((req.method == "GET" || is_head_request) &&
handle_file_request(req, res, is_head_request)) {
if ((req.method == "GET" || req.method == "HEAD") &&
handle_file_request(req, res)) {
return true;
}
@@ -7123,7 +7234,11 @@ inline bool Server::dispatch_request(Request &req, Response &res,
const auto &handler = x.second;
if (matcher->match(req)) {
handler(req, res);
req.matched_route = matcher->pattern();
if (!pre_request_handler_ ||
pre_request_handler_(req, res) != HandlerResponse::Handled) {
handler(req, res);
}
return true;
}
}
@@ -7250,7 +7365,11 @@ inline bool Server::dispatch_request_for_content_reader(
const auto &handler = x.second;
if (matcher->match(req)) {
handler(req, res, content_reader);
req.matched_route = matcher->pattern();
if (!pre_request_handler_ ||
pre_request_handler_(req, res) != HandlerResponse::Handled) {
handler(req, res, content_reader);
}
return true;
}
}
+2 -2
View File
@@ -87,7 +87,7 @@ if get_option('cpp-httplib_compile')
soversion: version.split('.')[0] + '.' + version.split('.')[1],
install: true
)
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, link_with: lib, sources: httplib_ch[1])
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, link_with: lib, sources: httplib_ch[1], version: version)
import('pkgconfig').generate(
lib,
@@ -98,7 +98,7 @@ if get_option('cpp-httplib_compile')
)
else
install_headers('httplib.h')
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, include_directories: '.')
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, include_directories: '.', version: version)
import('pkgconfig').generate(
name: 'cpp-httplib',
+345 -8
View File
@@ -3,7 +3,11 @@
#include <signal.h>
#ifndef _WIN32
#include <arpa/inet.h>
#include <curl/curl.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
#include <gtest/gtest.h>
@@ -2263,7 +2267,7 @@ TEST(NoContentTest, ContentLength) {
}
}
TEST(RoutingHandlerTest, PreRoutingHandler) {
TEST(RoutingHandlerTest, PreAndPostRoutingHandlers) {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
@@ -2354,6 +2358,63 @@ TEST(RoutingHandlerTest, PreRoutingHandler) {
}
}
TEST(RequestHandlerTest, PreRequestHandler) {
auto route_path = "/user/:user";
Server svr;
svr.Get("/hi", [](const Request &, Response &res) {
res.set_content("hi", "text/plain");
});
svr.Get(route_path, [](const Request &req, Response &res) {
res.set_content(req.path_params.at("user"), "text/plain");
});
svr.set_pre_request_handler([&](const Request &req, Response &res) {
if (req.matched_route == route_path) {
auto user = req.path_params.at("user");
if (user != "john") {
res.status = StatusCode::Forbidden_403;
res.set_content("error", "text/html");
return Server::HandlerResponse::Handled;
}
}
return Server::HandlerResponse::Unhandled;
});
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
{
auto res = cli.Get("/hi");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ("hi", res->body);
}
{
auto res = cli.Get("/user/john");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ("john", res->body);
}
{
auto res = cli.Get("/user/invalid-user");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::Forbidden_403, res->status);
EXPECT_EQ("error", res->body);
}
}
TEST(InvalidFormatTest, StatusCode) {
Server svr;
@@ -3098,6 +3159,47 @@ TEST_F(ServerTest, GetMethod200) {
EXPECT_EQ("Hello World!", res->body);
}
TEST(BenchmarkTest, SimpleGetPerformance) {
Server svr;
svr.Get("/benchmark", [&](const Request & /*req*/, Response &res) {
res.set_content("Benchmark Response", "text/plain");
});
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli("localhost", PORT);
const int NUM_REQUESTS = 50;
const int MAX_AVERAGE_MS = 5;
auto warmup = cli.Get("/benchmark");
ASSERT_TRUE(warmup);
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < NUM_REQUESTS; ++i) {
auto res = cli.Get("/benchmark");
ASSERT_TRUE(res) << "Request " << i << " failed";
EXPECT_EQ(StatusCode::OK_200, res->status);
}
auto end = std::chrono::high_resolution_clock::now();
auto total_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
double avg_ms = static_cast<double>(total_ms) / NUM_REQUESTS;
std::cout << "Standalone: " << NUM_REQUESTS << " requests in " << total_ms
<< "ms (avg: " << avg_ms << "ms)" << std::endl;
EXPECT_LE(avg_ms, MAX_AVERAGE_MS) << "Standalone test too slow: " << avg_ms << "ms (Issue #1777)";
}
TEST_F(ServerTest, GetEmptyFile) {
auto res = cli_.Get("/empty_file");
ASSERT_TRUE(res);
@@ -3725,6 +3827,50 @@ TEST_F(ServerTest, TooLongHeader) {
EXPECT_EQ(StatusCode::OK_200, res->status);
}
TEST_F(ServerTest, HeaderCountAtLimit) {
// Test with headers just under the 100 limit
httplib::Headers headers;
// Add 95 custom headers (the client will add Host, User-Agent, Accept, etc.)
// This should keep us just under the 100 header limit
for (int i = 0; i < 95; i++) {
std::string name = "X-Test-Header-" + std::to_string(i);
std::string value = "value" + std::to_string(i);
headers.emplace(name, value);
}
// This should work fine as we're under the limit
auto res = cli_.Get("/hi", headers);
EXPECT_TRUE(res);
if (res) {
EXPECT_EQ(StatusCode::OK_200, res->status);
}
}
TEST_F(ServerTest, HeaderCountExceedsLimit) {
// Test with many headers to exceed the 100 limit
httplib::Headers headers;
// Add 150 headers to definitely exceed the 100 limit
for (int i = 0; i < 150; i++) {
std::string name = "X-Test-Header-" + std::to_string(i);
std::string value = "value" + std::to_string(i);
headers.emplace(name, value);
}
// This should fail due to exceeding header count limit
auto res = cli_.Get("/hi", headers);
// The request should either fail or return 400 Bad Request
if (res) {
// If we get a response, it should be 400 Bad Request
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
} else {
// Or the request should fail entirely
EXPECT_FALSE(res);
}
}
TEST_F(ServerTest, PercentEncoding) {
auto res = cli_.Get("/e%6edwith%");
ASSERT_TRUE(res);
@@ -3762,6 +3908,32 @@ TEST_F(ServerTest, PlusSignEncoding) {
EXPECT_EQ("a +b", res->body);
}
TEST_F(ServerTest, HeaderCountSecurityTest) {
// This test simulates a potential DoS attack using many headers
// to verify our security fix prevents memory exhaustion
httplib::Headers attack_headers;
// Attempt to add many headers like an attacker would (200 headers to far exceed limit)
for (int i = 0; i < 200; i++) {
std::string name = "X-Attack-Header-" + std::to_string(i);
std::string value = "attack_payload_" + std::to_string(i);
attack_headers.emplace(name, value);
}
// Try to POST with excessive headers
auto res = cli_.Post("/", attack_headers, "test_data", "text/plain");
// Should either fail or return 400 Bad Request due to security limit
if (res) {
// If we get a response, it should be 400 Bad Request
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
} else {
// Request failed, which is the expected behavior for DoS protection
EXPECT_FALSE(res);
}
}
TEST_F(ServerTest, MultipartFormData) {
MultipartFormDataItems items = {
{"text1", "text default", "", ""},
@@ -3935,6 +4107,16 @@ TEST_F(ServerTest, GetStreamedWithRangeMultipart) {
EXPECT_EQ("267", res->get_header_value("Content-Length"));
EXPECT_EQ(false, res->has_header("Content-Range"));
EXPECT_EQ(267U, res->body.size());
// Check that both range contents are present
EXPECT_TRUE(res->body.find("bc\r\n") != std::string::npos);
EXPECT_TRUE(res->body.find("ef\r\n") != std::string::npos);
// Check that Content-Range headers are present for both ranges
EXPECT_TRUE(res->body.find("Content-Range: bytes 1-2/7") !=
std::string::npos);
EXPECT_TRUE(res->body.find("Content-Range: bytes 4-5/7") !=
std::string::npos);
}
TEST_F(ServerTest, GetStreamedWithTooManyRanges) {
@@ -3952,14 +4134,59 @@ TEST_F(ServerTest, GetStreamedWithTooManyRanges) {
EXPECT_EQ(0U, res->body.size());
}
TEST_F(ServerTest, GetStreamedWithNonAscendingRanges) {
auto res = cli_.Get("/streamed-with-range?error",
{{make_range_header({{0, -1}, {0, -1}})}});
TEST_F(ServerTest, GetStreamedWithOverwrapping) {
auto res =
cli_.Get("/streamed-with-range", {{make_range_header({{1, 4}, {2, 5}})}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
EXPECT_EQ("0", res->get_header_value("Content-Length"));
EXPECT_EQ(false, res->has_header("Content-Range"));
EXPECT_EQ(0U, res->body.size());
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ(5U, res->body.size());
// Check that overlapping ranges are coalesced into a single range
EXPECT_EQ("bcdef", res->body);
EXPECT_EQ("bytes 1-5/7", res->get_header_value("Content-Range"));
// Should be single range, not multipart
EXPECT_TRUE(res->has_header("Content-Range"));
EXPECT_EQ("text/plain", res->get_header_value("Content-Type"));
}
TEST_F(ServerTest, GetStreamedWithNonAscendingRanges) {
auto res =
cli_.Get("/streamed-with-range", {{make_range_header({{4, 5}, {0, 2}})}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ(268U, res->body.size());
// Check that both range contents are present
EXPECT_TRUE(res->body.find("ef\r\n") != std::string::npos);
EXPECT_TRUE(res->body.find("abc\r\n") != std::string::npos);
// Check that Content-Range headers are present for both ranges
EXPECT_TRUE(res->body.find("Content-Range: bytes 4-5/7") !=
std::string::npos);
EXPECT_TRUE(res->body.find("Content-Range: bytes 0-2/7") !=
std::string::npos);
}
TEST_F(ServerTest, GetStreamedWithDuplicateRanges) {
auto res =
cli_.Get("/streamed-with-range", {{make_range_header({{0, 2}, {0, 2}})}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ(269U, res->body.size());
// Check that both duplicate range contents are present
size_t first_abc = res->body.find("abc\r\n");
EXPECT_TRUE(first_abc != std::string::npos);
size_t second_abc = res->body.find("abc\r\n", first_abc + 1);
EXPECT_TRUE(second_abc != std::string::npos);
// Check that Content-Range headers are present for both ranges
size_t first_range = res->body.find("Content-Range: bytes 0-2/7");
EXPECT_TRUE(first_range != std::string::npos);
size_t second_range =
res->body.find("Content-Range: bytes 0-2/7", first_range + 1);
EXPECT_TRUE(second_range != std::string::npos);
}
TEST_F(ServerTest, GetStreamedWithRangesMoreThanTwoOverwrapping) {
@@ -4065,6 +4292,19 @@ TEST_F(ServerTest, GetWithRange4) {
EXPECT_EQ(std::string("fg"), res->body);
}
TEST_F(ServerTest, GetWithRange5) {
auto res = cli_.Get("/with-range", {
make_range_header({{0, 5}}),
{"Accept-Encoding", ""},
});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ("6", res->get_header_value("Content-Length"));
EXPECT_EQ(true, res->has_header("Content-Range"));
EXPECT_EQ("bytes 0-5/7", res->get_header_value("Content-Range"));
EXPECT_EQ(std::string("abcdef"), res->body);
}
TEST_F(ServerTest, GetWithRangeOffsetGreaterThanContent) {
auto res = cli_.Get("/with-range", {{make_range_header({{10000, 20000}})}});
ASSERT_TRUE(res);
@@ -5732,6 +5972,41 @@ TEST(KeepAliveTest, ReadTimeout) {
EXPECT_EQ("b", resb->body);
}
TEST(KeepAliveTest, MaxCount) {
size_t keep_alive_max_count = 3;
Server svr;
svr.set_keep_alive_max_count(keep_alive_max_count);
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
res.set_content("Hello World!", "text/plain");
});
auto listen_thread = std::thread([&svr] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
cli.set_keep_alive(true);
for (size_t i = 0; i < 5; i++) {
auto result = cli.Get("/hi");
ASSERT_TRUE(result);
EXPECT_EQ(StatusCode::OK_200, result->status);
if (i == keep_alive_max_count - 1) {
EXPECT_EQ("close", result->get_header_value("Connection"));
} else {
EXPECT_FALSE(result->has_header("Connection"));
}
}
}
TEST(KeepAliveTest, Issue1041) {
Server svr;
svr.set_keep_alive_timeout(3);
@@ -7918,6 +8193,68 @@ TEST(MultipartFormDataTest, ContentLength) {
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("200", response.substr(9, 3));
}
TEST(MultipartFormDataTest, AccessPartHeaders) {
auto handled = false;
Server svr;
svr.Post("/test", [&](const Request &req, Response &) {
ASSERT_EQ(2u, req.files.size());
auto it = req.files.begin();
ASSERT_EQ("text1", it->second.name);
ASSERT_EQ("text1", it->second.content);
ASSERT_EQ(1, it->second.headers.count("Content-Length"));
auto content_length = it->second.headers.find("CONTENT-length");
ASSERT_EQ("5", content_length->second);
ASSERT_EQ(3, it->second.headers.size());
++it;
ASSERT_EQ("text2", it->second.name);
ASSERT_EQ("text2", it->second.content);
auto &headers = it->second.headers;
ASSERT_EQ(3, headers.size());
auto custom_header = headers.find("x-whatever");
ASSERT_TRUE(custom_header != headers.end());
ASSERT_NE("customvalue", custom_header->second);
ASSERT_EQ("CustomValue", custom_header->second);
ASSERT_TRUE(headers.find("X-Test") == headers.end()); // text1 header
handled = true;
});
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
ASSERT_TRUE(handled);
});
svr.wait_until_ready();
auto req = "POST /test HTTP/1.1\r\n"
"Content-Type: multipart/form-data;boundary=--------\r\n"
"Content-Length: 232\r\n"
"\r\n----------\r\n"
"Content-Disposition: form-data; name=\"text1\"\r\n"
"Content-Length: 5\r\n"
"X-Test: 1\r\n"
"\r\n"
"text1"
"\r\n----------\r\n"
"Content-Disposition: form-data; name=\"text2\"\r\n"
"Content-Type: text/plain\r\n"
"X-Whatever: CustomValue\r\n"
"\r\n"
"text2"
"\r\n------------\r\n"
"That should be disregarded. Not even read";
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("200", response.substr(9, 3));
}
#endif
TEST(TaskQueueTest, IncreaseAtomicInteger) {