Skip to content

Commit 34651ef

Browse files
committed
Fix #121
1 parent 4f237af commit 34651ef

File tree

3 files changed

+108
-78
lines changed

3 files changed

+108
-78
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,20 @@ httplib::Params params{
150150
auto res = cli.Post("/post", params);
151151
```
152152

153+
### POST with Multipart Form Data
154+
155+
```c++
156+
httplib::MultipartFormDataItems items = {
157+
{ "text1", "text default", "", "" },
158+
{ "text2", "aωb", "", "" },
159+
{ "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
160+
{ "file2", "{\n \"world\", true\n}\n", "world.json", "application/json" },
161+
{ "file3", "", "", "application/octet-stream" },
162+
};
163+
164+
auto res = cli.Post("/multipart", items);
165+
```
166+
153167
### PUT
154168
155169
```c++

httplib.h

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ typedef int socket_t;
6767
#include <map>
6868
#include <memory>
6969
#include <mutex>
70+
#include <random>
7071
#include <regex>
7172
#include <string>
7273
#include <sys/stat.h>
@@ -138,6 +139,14 @@ struct MultipartFile {
138139
};
139140
typedef std::multimap<std::string, MultipartFile> MultipartFiles;
140141

142+
struct MultipartFormData {
143+
std::string name;
144+
std::string content;
145+
std::string filename;
146+
std::string content_type;
147+
};
148+
typedef std::vector<MultipartFormData> MultipartFormDataItems;
149+
141150
struct Request {
142151
std::string version;
143152
std::string method;
@@ -340,6 +349,11 @@ class Client {
340349
std::shared_ptr<Response> Post(const char *path, const Headers &headers,
341350
const Params &params);
342351

352+
std::shared_ptr<Response> Post(const char *path,
353+
const MultipartFormDataItems &items);
354+
std::shared_ptr<Response> Post(const char *path, const Headers &headers,
355+
const MultipartFormDataItems &items);
356+
343357
std::shared_ptr<Response> Put(const char *path, const std::string &body,
344358
const char *content_type);
345359
std::shared_ptr<Response> Put(const char *path, const Headers &headers,
@@ -551,9 +565,7 @@ inline std::string base64_encode(const std::string &in) {
551565
}
552566
}
553567

554-
if (valb > -6) {
555-
out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]);
556-
}
568+
if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); }
557569

558570
while (out.size() % 4) {
559571
out.push_back('=');
@@ -1231,16 +1243,13 @@ bool read_content(Stream &strm, T &x, uint64_t payload_max_length, int &status,
12311243
template <typename T> inline int write_headers(Stream &strm, const T &info) {
12321244
auto write_len = 0;
12331245
for (const auto &x : info.headers) {
1234-
auto len = strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str());
1235-
if (len < 0) {
1236-
return len;
1237-
}
1246+
auto len =
1247+
strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str());
1248+
if (len < 0) { return len; }
12381249
write_len += len;
12391250
}
12401251
auto len = strm.write("\r\n");
1241-
if (len < 0) {
1242-
return len;
1243-
}
1252+
if (len < 0) { return len; }
12441253
write_len += len;
12451254
return write_len;
12461255
}
@@ -1262,9 +1271,7 @@ inline int write_content_chunked(Stream &strm, const T &x) {
12621271
}
12631272

12641273
auto len = strm.write(chunk.c_str(), chunk.size());
1265-
if (len < 0) {
1266-
return len;
1267-
}
1274+
if (len < 0) { return len; }
12681275
write_len += len;
12691276
}
12701277
return write_len;
@@ -1444,6 +1451,22 @@ inline std::string to_lower(const char *beg, const char *end) {
14441451
return out;
14451452
}
14461453

1454+
inline std::string make_multipart_data_boundary() {
1455+
static const char data[] =
1456+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1457+
1458+
std::random_device seed_gen;
1459+
std::mt19937 engine(seed_gen());
1460+
1461+
std::string result = "--cpp-httplib-form-data-";
1462+
1463+
for (auto i = 0; i < 16; i++) {
1464+
result += data[engine() % (sizeof(data) - 1)];
1465+
}
1466+
1467+
return result;
1468+
}
1469+
14471470
inline void make_range_header_core(std::string &) {}
14481471

14491472
template <typename uint64_t>
@@ -1486,9 +1509,9 @@ inline std::pair<std::string, std::string> make_range_header(uint64_t value,
14861509
return std::make_pair("Range", field);
14871510
}
14881511

1489-
1490-
inline std::pair<std::string, std::string>
1491-
make_basic_authentication_header(const std::string& username, const std::string& password) {
1512+
inline std::pair<std::string, std::string>
1513+
make_basic_authentication_header(const std::string &username,
1514+
const std::string &password) {
14921515
auto field = "Basic " + detail::base64_encode(username + ":" + password);
14931516
return std::make_pair("Authorization", field);
14941517
}
@@ -1583,9 +1606,7 @@ inline int Stream::write_format(const char *fmt, const Args &... args) {
15831606
#else
15841607
auto n = snprintf(buf, bufsiz - 1, fmt, args...);
15851608
#endif
1586-
if (n <= 0) {
1587-
return n;
1588-
}
1609+
if (n <= 0) { return n; }
15891610

15901611
if (n >= bufsiz - 1) {
15911612
std::vector<char> glowable_buf(bufsiz);
@@ -1769,7 +1790,7 @@ inline bool Server::write_response(Stream &strm, bool last_connection,
17691790

17701791
// Response line
17711792
if (!strm.write_format("HTTP/1.1 %d %s\r\n", res.status,
1772-
detail::status_message(res.status))) {
1793+
detail::status_message(res.status))) {
17731794
return false;
17741795
}
17751796

@@ -1811,20 +1832,14 @@ inline bool Server::write_response(Stream &strm, bool last_connection,
18111832
res.set_header("Content-Length", length.c_str());
18121833
}
18131834

1814-
if (!detail::write_headers(strm, res)) {
1815-
return false;
1816-
}
1835+
if (!detail::write_headers(strm, res)) { return false; }
18171836

18181837
// Body
18191838
if (req.method != "HEAD") {
18201839
if (!res.body.empty()) {
1821-
if (!strm.write(res.body.c_str(), res.body.size())) {
1822-
return false;
1823-
}
1840+
if (!strm.write(res.body.c_str(), res.body.size())) { return false; }
18241841
} else if (res.content_producer) {
1825-
if (!detail::write_content_chunked(strm, res)) {
1826-
return false;
1827-
}
1842+
if (!detail::write_content_chunked(strm, res)) { return false; }
18281843
}
18291844
}
18301845

@@ -2325,6 +2340,45 @@ Client::Post(const char *path, const Headers &headers, const Params &params) {
23252340
return Post(path, headers, query, "application/x-www-form-urlencoded");
23262341
}
23272342

2343+
inline std::shared_ptr<Response>
2344+
Client::Post(const char *path, const MultipartFormDataItems &items) {
2345+
return Post(path, Headers(), items);
2346+
}
2347+
2348+
inline std::shared_ptr<Response>
2349+
Client::Post(const char *path, const Headers &headers,
2350+
const MultipartFormDataItems &items) {
2351+
Request req;
2352+
req.method = "POST";
2353+
req.headers = headers;
2354+
req.path = path;
2355+
2356+
auto boundary = detail::make_multipart_data_boundary();
2357+
2358+
req.headers.emplace("Content-Type",
2359+
"multipart/form-data; boundary=" + boundary);
2360+
2361+
for (const auto &item : items) {
2362+
req.body += "--" + boundary + "\r\n";
2363+
req.body += "Content-Disposition: form-data; name=\"" + item.name + "\"";
2364+
if (!item.filename.empty()) {
2365+
req.body += "; filename=\"" + item.filename + "\"";
2366+
}
2367+
req.body += "\r\n";
2368+
if (!item.content_type.empty()) {
2369+
req.body += "Content-Type: " + item.content_type + "\r\n";
2370+
}
2371+
req.body += "\r\n";
2372+
req.body += item.content + "\r\n";
2373+
}
2374+
2375+
req.body += "--" + boundary + "--\r\n";
2376+
2377+
auto res = std::make_shared<Response>();
2378+
2379+
return send(req, *res) ? res : nullptr;
2380+
}
2381+
23282382
inline std::shared_ptr<Response> Client::Put(const char *path,
23292383
const std::string &body,
23302384
const char *content_type) {

test/test.cc

Lines changed: 11 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,8 @@ TEST(CancelTest, NoCancel) {
264264
httplib::Client cli(host, port, sec);
265265
#endif
266266

267-
httplib::Headers headers;
268267
auto res =
269-
cli.Get("/range/32", headers, [](uint64_t, uint64_t) { return true; });
268+
cli.Get("/range/32", [](uint64_t, uint64_t) { return true; });
270269
ASSERT_TRUE(res != nullptr);
271270
EXPECT_EQ(res->body, "abcdefghijklmnopqrstuvwxyzabcdef");
272271
EXPECT_EQ(200, res->status);
@@ -284,9 +283,8 @@ TEST(CancelTest, WithCancelSmallPayload) {
284283
httplib::Client cli(host, port, sec);
285284
#endif
286285

287-
httplib::Headers headers;
288286
auto res =
289-
cli.Get("/range/32", headers, [](uint64_t, uint64_t) { return false; });
287+
cli.Get("/range/32", [](uint64_t, uint64_t) { return false; });
290288
ASSERT_TRUE(res == nullptr);
291289
}
292290

@@ -964,53 +962,17 @@ TEST_F(ServerTest, EndWithPercentCharacterInQuery) {
964962
}
965963

966964
TEST_F(ServerTest, MultipartFormData) {
967-
Request req;
968-
req.method = "POST";
969-
req.path = "/multipart";
970-
971-
std::string host_and_port;
972-
host_and_port += HOST;
973-
host_and_port += ":";
974-
host_and_port += std::to_string(PORT);
975-
976-
req.headers.emplace("Host", host_and_port.c_str());
977-
req.headers.emplace("Accept", "*/*");
978-
req.headers.emplace("User-Agent", "cpp-httplib/0.1");
979-
980-
req.headers.emplace(
981-
"Content-Type",
982-
"multipart/form-data; boundary=----WebKitFormBoundarysBREP3G013oUrLB4");
983-
984-
req.body =
985-
"------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
986-
"Content-Disposition: form-data; name=\"text1\"\r\n"
987-
"\r\n"
988-
"text default\r\n"
989-
"------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
990-
"Content-Disposition: form-data; name=\"text2\"\r\n"
991-
"\r\n"
992-
"aωb\r\n"
993-
"------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
994-
"Content-Disposition: form-data; name=\"file1\"; filename=\"hello.txt\"\r\n"
995-
"Content-Type: text/plain\r\n"
996-
"\r\n"
997-
"h\ne\n\nl\nl\no\n\r\n"
998-
"------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
999-
"Content-Disposition: form-data; name=\"file2\"; filename=\"world.json\"\r\n"
1000-
"Content-Type: application/json\r\n"
1001-
"\r\n"
1002-
"{\n \"world\", true\n}\n\r\n"
1003-
"------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
1004-
"content-disposition: form-data; name=\"file3\"; filename=\"\"\r\n"
1005-
"content-type: application/octet-stream\r\n"
1006-
"\r\n"
1007-
"\r\n"
1008-
"------WebKitFormBoundarysBREP3G013oUrLB4--\r\n";
965+
MultipartFormDataItems items = {
966+
{ "text1", "text default", "", "" },
967+
{ "text2", "aωb", "", "" },
968+
{ "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
969+
{ "file2", "{\n \"world\", true\n}\n", "world.json", "application/json" },
970+
{ "file3", "", "", "application/octet-stream" },
971+
};
1009972

1010-
auto res = std::make_shared<Response>();
1011-
auto ret = cli_.send(req, *res);
973+
auto res = cli_.Post("/multipart", items);
1012974

1013-
ASSERT_TRUE(ret);
975+
ASSERT_TRUE(res != nullptr);
1014976
EXPECT_EQ(200, res->status);
1015977
}
1016978

0 commit comments

Comments
 (0)