Skip to content

Commit 841d653

Browse files
committed
Give DataSink's optional callbacks safe defaults
DataSink has four callbacks, but only write is assigned by every writer that hands a sink to a content provider: write_content_with_progress() write, is_writable write_content_without_length() write, is_writable, done write_content_chunked() all four send_with_content_provider...() write get_multipart_content_provider() write, done (cur_sink) A provider that calls one of the unassigned ones invokes an empty std::function and throws std::bad_function_call. Nothing on that path catches it, so it unwinds out of the thread running the provider and terminates the process. The README's own idiom is enough to hit it: sink.done() is documented for the without-length overload, but a provider registered through set_content_provider() with a length gets a sink where done is empty. Default the three optional callbacks instead. A sink is writable unless a writer says otherwise, and a sink that cannot carry trailers still has to finish, so done_with_trailer() falls back to done(). Capturing this for that is safe because DataSink is neither copyable nor movable. A no-op done() alone would only trade the crash for a hang on the two length-framed paths: both loop until offset reaches the promised length, so a provider that reports itself done without writing would be called again immediately, forever. Both now record that the provider finished and stop, and the short body is reported as a write error. The client path gains that check for the compressor-failure exit as well, which used to send a truncated request body without reporting anything. cur_sink in get_multipart_content_provider() now forwards is_writable from the outer sink, so a provider item asking whether it may keep going gets the stream's answer rather than the default.
1 parent 254e576 commit 841d653

2 files changed

Lines changed: 153 additions & 6 deletions

File tree

httplib.h

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,9 +1429,16 @@ class DataSink {
14291429
DataSink &operator=(DataSink &&) = delete;
14301430

14311431
std::function<bool(const char *data, size_t data_len)> write;
1432-
std::function<bool()> is_writable;
1433-
std::function<void()> done;
1434-
std::function<void(const Headers &trailer)> done_with_trailer;
1432+
1433+
// Only `write` is mandatory. The rest are defaulted so that a provider
1434+
// calling one on a writer that does not set it gets sensible behaviour
1435+
// rather than std::bad_function_call thrown from a worker thread. Capturing
1436+
// `this` is safe: DataSink is neither copyable nor movable.
1437+
std::function<bool()> is_writable = []() { return true; };
1438+
std::function<void()> done = []() {};
1439+
std::function<void(const Headers &trailer)> done_with_trailer =
1440+
[this](const Headers & /*trailer*/) { done(); };
1441+
14351442
std::ostream os;
14361443

14371444
private:
@@ -8298,6 +8305,7 @@ inline bool write_content_with_progress(Stream &strm,
82988305
size_t end_offset = offset + length;
82998306
size_t start_offset = offset;
83008307
auto ok = true;
8308+
auto finished = false;
83018309
DataSink data_sink;
83028310

83038311
data_sink.write = [&](const char *d, size_t l) -> bool {
@@ -8321,7 +8329,12 @@ inline bool write_content_with_progress(Stream &strm,
83218329

83228330
data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
83238331

8324-
while (offset < end_offset && !is_shutting_down()) {
8332+
// The body is framed by `length`, so a provider that reports itself done
8333+
// early has truncated it. Record that and let the short-body check below
8334+
// fail the write, rather than calling the provider again forever.
8335+
data_sink.done = [&]() { finished = true; };
8336+
8337+
while (offset < end_offset && !finished && !is_shutting_down()) {
83258338
if (!strm.wait_writable() || !strm.is_peer_alive()) {
83268339
error = Error::Write;
83278340
return false;
@@ -8334,7 +8347,7 @@ inline bool write_content_with_progress(Stream &strm,
83348347
}
83358348
}
83368349

8337-
if (offset < end_offset) { // exited due to is_shutting_down(), not completion
8350+
if (offset < end_offset) { // done() called early, or is_shutting_down()
83388351
error = Error::Write;
83398352
return false;
83408353
}
@@ -15312,6 +15325,7 @@ ClientImpl::send_with_content_provider_and_receiver(
1531215325

1531315326
if (content_provider) {
1531415327
auto ok = true;
15328+
auto finished = false;
1531515329
size_t offset = 0;
1531615330
DataSink data_sink;
1531715331

@@ -15335,13 +15349,27 @@ ClientImpl::send_with_content_provider_and_receiver(
1533515349
return ok;
1533615350
};
1533715351

15338-
while (ok && offset < content_length) {
15352+
// As in detail::write_content_with_progress(): the body is framed by
15353+
// content_length, so a provider that finishes early has truncated it.
15354+
// Stop and report that instead of calling the provider forever.
15355+
data_sink.done = [&]() { finished = true; };
15356+
15357+
while (ok && !finished && offset < content_length) {
1533915358
if (!content_provider(offset, content_length - offset, data_sink)) {
1534015359
error = Error::Canceled;
1534115360
output_error_log(error, &req);
1534215361
return nullptr;
1534315362
}
1534415363
}
15364+
15365+
// A short body here means either the provider stopped early or the
15366+
// compressor gave up. The branch below reports a failing compressor as
15367+
// Error::Compression, so keep the two distinguishable.
15368+
if (offset < content_length) {
15369+
error = ok ? Error::Write : Error::Compression;
15370+
output_error_log(error, &req);
15371+
return nullptr;
15372+
}
1534515373
} else {
1534615374
if (!compressor->compress(body, content_length, true,
1534715375
[&](const char *data, size_t data_len) {
@@ -15644,6 +15672,9 @@ inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider(
1564415672
DataSink cur_sink;
1564515673
auto has_data = true;
1564615674
cur_sink.write = sink.write;
15675+
// Forward is_writable so a provider item asking whether it may keep
15676+
// going gets the outer sink's answer rather than the default `true`.
15677+
cur_sink.is_writable = sink.is_writable;
1564715678
cur_sink.done = [&]() { has_data = false; };
1564815679

1564915680
if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) {

test/test.cc

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10502,6 +10502,122 @@ TEST(ClientProblemDetectionTest, ContentProvider) {
1050210502
}
1050310503
}
1050410504

10505+
TEST(DataSinkTest, OptionalCallbacksAreCallableByDefault) {
10506+
// A writer only has to assign `write`. The other three used to be left as
10507+
// empty std::functions, so a provider calling one threw
10508+
// std::bad_function_call out of the thread running it and took the whole
10509+
// process down.
10510+
DataSink sink;
10511+
10512+
std::string written;
10513+
sink.write = [&](const char *data, size_t data_len) {
10514+
written.append(data, data_len);
10515+
return true;
10516+
};
10517+
10518+
EXPECT_TRUE(sink.is_writable());
10519+
sink.done();
10520+
sink.done_with_trailer(Headers{{"X-Trailer", "value"}});
10521+
EXPECT_TRUE(written.empty());
10522+
}
10523+
10524+
TEST(DataSinkTest, DoneWithTrailerFallsBackToDone) {
10525+
// A sink that cannot carry trailers still has to finish.
10526+
DataSink sink;
10527+
10528+
auto done_count = 0;
10529+
sink.done = [&]() { done_count++; };
10530+
10531+
sink.done_with_trailer(Headers{{"X-Trailer", "value"}});
10532+
EXPECT_EQ(1, done_count);
10533+
}
10534+
10535+
TEST(DataSinkTest, LengthFramedProviderMayCallDoneAfterWritingEverything) {
10536+
Server svr;
10537+
10538+
const std::string body(4096, 'x');
10539+
10540+
svr.Get("/", [&](const Request & /*req*/, Response &res) {
10541+
res.set_content_provider(body.size(), "text/plain",
10542+
[&](size_t offset, size_t length, DataSink &sink) {
10543+
sink.write(body.data() + offset, length);
10544+
sink.done();
10545+
return true;
10546+
});
10547+
});
10548+
10549+
auto port = svr.bind_to_any_port(HOST);
10550+
auto listen_thread = std::thread([&svr]() { svr.listen_after_bind(); });
10551+
auto se = detail::scope_exit([&] {
10552+
svr.stop();
10553+
listen_thread.join();
10554+
ASSERT_FALSE(svr.is_running());
10555+
});
10556+
10557+
svr.wait_until_ready();
10558+
10559+
Client cli(HOST, port);
10560+
auto res = cli.Get("/");
10561+
10562+
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
10563+
EXPECT_EQ(StatusCode::OK_200, res->status);
10564+
EXPECT_EQ(body, res->body);
10565+
}
10566+
10567+
TEST(DataSinkTest, LengthFramedProviderThatFinishesEarlyFailsTheResponse) {
10568+
Server svr;
10569+
10570+
svr.Get("/", [](const Request & /*req*/, Response &res) {
10571+
res.set_content_provider(
10572+
1024, "text/plain",
10573+
[](size_t /*offset*/, size_t /*length*/, DataSink &sink) {
10574+
sink.write("hello", 5);
10575+
sink.done(); // short of the 1024 bytes the response promised
10576+
return true;
10577+
});
10578+
});
10579+
10580+
auto port = svr.bind_to_any_port(HOST);
10581+
auto listen_thread = std::thread([&svr]() { svr.listen_after_bind(); });
10582+
auto se = detail::scope_exit([&] {
10583+
svr.stop();
10584+
listen_thread.join();
10585+
ASSERT_FALSE(svr.is_running());
10586+
});
10587+
10588+
svr.wait_until_ready();
10589+
10590+
Client cli(HOST, port);
10591+
cli.set_read_timeout(5, 0);
10592+
10593+
// The response is short of its Content-Length, so the client cannot read a
10594+
// complete body. What matters is that this returns at all: a no-op done()
10595+
// would leave the writer looping over a provider that never makes progress.
10596+
auto res = cli.Get("/");
10597+
EXPECT_FALSE(res);
10598+
}
10599+
10600+
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
10601+
TEST(DataSinkTest, CompressedRequestProviderThatFinishesEarlyFails) {
10602+
// The compressed path builds the whole body before connecting, so this
10603+
// fails client-side and never reaches a server.
10604+
Client cli(HOST, PORT);
10605+
cli.set_compress(true);
10606+
10607+
auto res = cli.Post(
10608+
"/", 1024,
10609+
[](size_t /*offset*/, size_t /*length*/, DataSink &sink) {
10610+
sink.write("hello", 5);
10611+
sink.done(); // short of the 1024 bytes the request announced
10612+
return true;
10613+
},
10614+
"text/plain");
10615+
10616+
ASSERT_FALSE(res);
10617+
EXPECT_EQ(Error::Write, res.error());
10618+
}
10619+
#endif
10620+
1050510621
TEST(ErrorHandlerWithContentProviderTest, ErrorHandler) {
1050610622
Server svr;
1050710623

0 commit comments

Comments
 (0)