Skip to content
This repository was archived by the owner on Apr 30, 2025. It is now read-only.

Commit 88a9278

Browse files
committed
1 parent 9bb3ca8 commit 88a9278

File tree

3 files changed

+137
-46
lines changed

3 files changed

+137
-46
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,27 @@ svr.Get("/chunked", [&](const Request& req, Response& res) {
347347
});
348348
```
349349

350+
With trailer:
351+
352+
```cpp
353+
svr.Get("/chunked", [&](const Request& req, Response& res) {
354+
res.set_header("Trailer", "Dummy1, Dummy2");
355+
res.set_chunked_content_provider(
356+
"text/plain",
357+
[](size_t offset, DataSink &sink) {
358+
sink.write("123", 3);
359+
sink.write("345", 3);
360+
sink.write("789", 3);
361+
sink.done_with_trailer({
362+
{"Dummy1", "DummyVal1"},
363+
{"Dummy2", "DummyVal2"}
364+
});
365+
return true;
366+
}
367+
);
368+
});
369+
```
370+
350371
### 'Expect: 100-continue' handler
351372

352373
By default, the server sends a `100 Continue` response for an `Expect: 100-continue` header.

httplib.h

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ class DataSink {
371371

372372
std::function<bool(const char *data, size_t data_len)> write;
373373
std::function<void()> done;
374+
std::function<void(const Headers &trailer)> done_with_trailer;
374375
std::ostream os;
375376

376377
private:
@@ -3525,7 +3526,8 @@ inline bool read_content_without_length(Stream &strm,
35253526
return true;
35263527
}
35273528

3528-
inline bool read_content_chunked(Stream &strm,
3529+
template <typename T>
3530+
inline bool read_content_chunked(Stream &strm, T &x,
35293531
ContentReceiverWithProgress out) {
35303532
const auto bufsiz = 16;
35313533
char buf[bufsiz];
@@ -3551,15 +3553,29 @@ inline bool read_content_chunked(Stream &strm,
35513553

35523554
if (!line_reader.getline()) { return false; }
35533555

3554-
if (strcmp(line_reader.ptr(), "\r\n")) { break; }
3556+
if (strcmp(line_reader.ptr(), "\r\n")) { return false; }
35553557

35563558
if (!line_reader.getline()) { return false; }
35573559
}
35583560

3559-
if (chunk_len == 0) {
3560-
// Reader terminator after chunks
3561-
if (!line_reader.getline() || strcmp(line_reader.ptr(), "\r\n"))
3562-
return false;
3561+
assert(chunk_len == 0);
3562+
3563+
// Trailer
3564+
if (!line_reader.getline()) { return false; }
3565+
3566+
while (strcmp(line_reader.ptr(), "\r\n")) {
3567+
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
3568+
3569+
// Exclude line terminator
3570+
constexpr auto line_terminator_len = 2;
3571+
auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
3572+
3573+
parse_header(line_reader.ptr(), end,
3574+
[&](std::string &&key, std::string &&val) {
3575+
x.headers.emplace(std::move(key), std::move(val));
3576+
});
3577+
3578+
if (!line_reader.getline()) { return false; }
35633579
}
35643580

35653581
return true;
@@ -3629,7 +3645,7 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
36293645
auto exceed_payload_max_length = false;
36303646

36313647
if (is_chunked_transfer_encoding(x.headers)) {
3632-
ret = read_content_chunked(strm, out);
3648+
ret = read_content_chunked(strm, x, out);
36333649
} else if (!has_header(x.headers, "Content-Length")) {
36343650
ret = read_content_without_length(strm, out);
36353651
} else {
@@ -3785,7 +3801,7 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider,
37853801
return ok;
37863802
};
37873803

3788-
data_sink.done = [&](void) {
3804+
auto done_with_trailer = [&](const Headers *trailer) {
37893805
if (!ok) { return; }
37903806

37913807
data_available = false;
@@ -3803,16 +3819,36 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider,
38033819
if (!payload.empty()) {
38043820
// Emit chunked response header and footer for each chunk
38053821
auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n";
3806-
if (!write_data(strm, chunk.data(), chunk.size())) {
3822+
if (!strm.is_writable() ||
3823+
!write_data(strm, chunk.data(), chunk.size())) {
38073824
ok = false;
38083825
return;
38093826
}
38103827
}
38113828

3812-
static const std::string done_marker("0\r\n\r\n");
3829+
static const std::string done_marker("0\r\n");
38133830
if (!write_data(strm, done_marker.data(), done_marker.size())) {
38143831
ok = false;
38153832
}
3833+
3834+
// Trailer
3835+
if (trailer) {
3836+
for (const auto &kv : *trailer) {
3837+
std::string field_line = kv.first + ": " + kv.second + "\r\n";
3838+
if (!write_data(strm, field_line.data(), field_line.size())) {
3839+
ok = false;
3840+
}
3841+
}
3842+
}
3843+
3844+
static const std::string crlf("\r\n");
3845+
if (!write_data(strm, crlf.data(), crlf.size())) { ok = false; }
3846+
};
3847+
3848+
data_sink.done = [&](void) { done_with_trailer(nullptr); };
3849+
3850+
data_sink.done_with_trailer = [&](const Headers &trailer) {
3851+
done_with_trailer(&trailer);
38163852
};
38173853

38183854
while (data_available && !is_shutting_down()) {

test/test.cc

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ TEST(ParseMultipartBoundaryTest, ValueWithQuote) {
186186
}
187187

188188
TEST(ParseMultipartBoundaryTest, ValueWithCharset) {
189-
string content_type = "multipart/mixed; boundary=THIS_STRING_SEPARATES;charset=UTF-8";
189+
string content_type =
190+
"multipart/mixed; boundary=THIS_STRING_SEPARATES;charset=UTF-8";
190191
string boundary;
191192
auto ret = detail::parse_multipart_boundary(content_type, boundary);
192193
EXPECT_TRUE(ret);
@@ -1710,6 +1711,30 @@ class ServerTest : public ::testing::Test {
17101711
delete i;
17111712
});
17121713
})
1714+
.Get("/streamed-chunked-with-trailer",
1715+
[&](const Request & /*req*/, Response &res) {
1716+
auto i = new int(0);
1717+
res.set_header("Trailer", "Dummy1, Dummy2");
1718+
res.set_chunked_content_provider(
1719+
"text/plain",
1720+
[i](size_t /*offset*/, DataSink &sink) {
1721+
switch (*i) {
1722+
case 0: sink.os << "123"; break;
1723+
case 1: sink.os << "456"; break;
1724+
case 2: sink.os << "789"; break;
1725+
case 3: {
1726+
sink.done_with_trailer(
1727+
{{"Dummy1", "DummyVal1"}, {"Dummy2", "DummyVal2"}});
1728+
} break;
1729+
}
1730+
(*i)++;
1731+
return true;
1732+
},
1733+
[i](bool success) {
1734+
EXPECT_TRUE(success);
1735+
delete i;
1736+
});
1737+
})
17131738
.Get("/streamed",
17141739
[&](const Request & /*req*/, Response &res) {
17151740
res.set_content_provider(
@@ -1801,39 +1826,39 @@ class ServerTest : public ::testing::Test {
18011826
}
18021827
})
18031828
.Post("/multipart/multi_file_values",
1804-
[&](const Request &req, Response & /*res*/) {
1805-
EXPECT_EQ(5u, req.files.size());
1806-
ASSERT_TRUE(!req.has_file("???"));
1807-
ASSERT_TRUE(req.body.empty());
1829+
[&](const Request &req, Response & /*res*/) {
1830+
EXPECT_EQ(5u, req.files.size());
1831+
ASSERT_TRUE(!req.has_file("???"));
1832+
ASSERT_TRUE(req.body.empty());
18081833

1809-
{
1834+
{
18101835
const auto &text_value = req.get_file_values("text");
18111836
EXPECT_EQ(text_value.size(), 1);
18121837
auto &text = text_value[0];
18131838
EXPECT_TRUE(text.filename.empty());
18141839
EXPECT_EQ("default text", text.content);
1815-
}
1816-
{
1817-
const auto &text1_values = req.get_file_values("multi_text1");
1818-
EXPECT_EQ(text1_values.size(), 2);
1819-
EXPECT_EQ("aaaaa", text1_values[0].content);
1820-
EXPECT_EQ("bbbbb", text1_values[1].content);
1821-
}
1822-
1823-
{
1824-
const auto &file1_values = req.get_file_values("multi_file1");
1825-
EXPECT_EQ(file1_values.size(), 2);
1826-
auto file1 = file1_values[0];
1827-
EXPECT_EQ(file1.filename, "hello.txt");
1828-
EXPECT_EQ(file1.content_type, "text/plain");
1829-
EXPECT_EQ("h\ne\n\nl\nl\no\n", file1.content);
1830-
1831-
auto file2 = file1_values[1];
1832-
EXPECT_EQ(file2.filename, "world.json");
1833-
EXPECT_EQ(file2.content_type, "application/json");
1834-
EXPECT_EQ("{\n \"world\", true\n}\n", file2.content);
1835-
}
1836-
})
1840+
}
1841+
{
1842+
const auto &text1_values = req.get_file_values("multi_text1");
1843+
EXPECT_EQ(text1_values.size(), 2);
1844+
EXPECT_EQ("aaaaa", text1_values[0].content);
1845+
EXPECT_EQ("bbbbb", text1_values[1].content);
1846+
}
1847+
1848+
{
1849+
const auto &file1_values = req.get_file_values("multi_file1");
1850+
EXPECT_EQ(file1_values.size(), 2);
1851+
auto file1 = file1_values[0];
1852+
EXPECT_EQ(file1.filename, "hello.txt");
1853+
EXPECT_EQ(file1.content_type, "text/plain");
1854+
EXPECT_EQ("h\ne\n\nl\nl\no\n", file1.content);
1855+
1856+
auto file2 = file1_values[1];
1857+
EXPECT_EQ(file2.filename, "world.json");
1858+
EXPECT_EQ(file2.content_type, "application/json");
1859+
EXPECT_EQ("{\n \"world\", true\n}\n", file2.content);
1860+
}
1861+
})
18371862
.Post("/empty",
18381863
[&](const Request &req, Response &res) {
18391864
EXPECT_EQ(req.body, "");
@@ -2680,13 +2705,14 @@ TEST_F(ServerTest, MultipartFormData) {
26802705

26812706
TEST_F(ServerTest, MultipartFormDataMultiFileValues) {
26822707
MultipartFormDataItems items = {
2683-
{"text", "default text", "", ""},
2708+
{"text", "default text", "", ""},
26842709

2685-
{"multi_text1", "aaaaa", "", ""},
2686-
{"multi_text1", "bbbbb", "", ""},
2710+
{"multi_text1", "aaaaa", "", ""},
2711+
{"multi_text1", "bbbbb", "", ""},
26872712

2688-
{"multi_file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain"},
2689-
{"multi_file1", "{\n \"world\", true\n}\n", "world.json", "application/json"},
2713+
{"multi_file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain"},
2714+
{"multi_file1", "{\n \"world\", true\n}\n", "world.json",
2715+
"application/json"},
26902716
};
26912717

26922718
auto res = cli_.Post("/multipart/multi_file_values", items);
@@ -2920,6 +2946,15 @@ TEST_F(ServerTest, GetStreamedChunked2) {
29202946
EXPECT_EQ(std::string("123456789"), res->body);
29212947
}
29222948

2949+
TEST_F(ServerTest, GetStreamedChunkedWithTrailer) {
2950+
auto res = cli_.Get("/streamed-chunked-with-trailer");
2951+
ASSERT_TRUE(res);
2952+
EXPECT_EQ(200, res->status);
2953+
EXPECT_EQ(std::string("123456789"), res->body);
2954+
EXPECT_EQ(std::string("DummyVal1"), res->get_header_value("Dummy1"));
2955+
EXPECT_EQ(std::string("DummyVal2"), res->get_header_value("Dummy2"));
2956+
}
2957+
29232958
TEST_F(ServerTest, LargeChunkedPost) {
29242959
Request req;
29252960
req.method = "POST";
@@ -3906,9 +3941,8 @@ TEST(ServerStopTest, StopServerWithChunkedTransmission) {
39063941

39073942
TEST(ServerStopTest, ClientAccessAfterServerDown) {
39083943
httplib::Server svr;
3909-
svr.Post("/hi", [&](const httplib::Request & /*req*/, httplib::Response &res) {
3910-
res.status = 200;
3911-
});
3944+
svr.Post("/hi", [&](const httplib::Request & /*req*/,
3945+
httplib::Response &res) { res.status = 200; });
39123946

39133947
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
39143948

0 commit comments

Comments
 (0)