diff --git a/httplib.h b/httplib.h index 1c98cc80bc..b983bdc5ae 100644 --- a/httplib.h +++ b/httplib.h @@ -11777,7 +11777,16 @@ Server::write_content_with_provider(Stream &strm, const Request &req, }; if (res.content_length_ > 0) { - if (req.ranges.empty()) { + // Only a 206 response is served as a partial representation. That is the + // condition `apply_ranges()` used when it decided the Content-Length and + // the multipart boundary, and `detail::range_error()` only validates + // `req.ranges` against the content length for a 2xx status. Honouring the + // ranges here under any other status would write a body that disagrees + // with the header we already sent, from an offset nothing has checked. + auto partial = + !req.ranges.empty() && res.status == StatusCode::PartialContent_206; + + if (!partial) { return detail::write_content(strm, res.content_provider_, 0, res.content_length_, is_shutting_down); } else if (req.ranges.size() == 1) { diff --git a/test/test.cc b/test/test.cc index b71171f75d..2492dab45f 100644 --- a/test/test.cc +++ b/test/test.cc @@ -4419,6 +4419,24 @@ class ServerTest : public ::testing::Test { delete data; }); }) + .Get("/streamed-with-range-and-error-status", + [&](const Request & /*req*/, Response &res) { + auto data = new std::string("abcdefg"); + res.status = StatusCode::Forbidden_403; + res.set_content_provider( + data->size(), "text/plain", + [data](size_t offset, size_t length, DataSink &sink) { + size_t DATA_CHUNK_SIZE = 4; + const auto &d = *data; + auto out_len = + std::min(static_cast(length), DATA_CHUNK_SIZE); + auto ret = + sink.write(&d[static_cast(offset)], out_len); + EXPECT_TRUE(ret); + return true; + }, + [data](bool /*success*/) { delete data; }); + }) .Get("/streamed-cancel", [&](const Request & /*req*/, Response &res) { res.set_content_provider( @@ -5932,6 +5950,27 @@ TEST_F(ServerTest, GetStreamedWithRangeSuffix2) { EXPECT_EQ(0U, res->body.size()); } +TEST_F(ServerTest, GetStreamedWithRangeAndErrorStatus) { + // The handler answers with a non-2xx status, so `detail::range_error()` + // never validated the ranges and `apply_ranges()` reported the full + // content length. The body must match that header and the content provider + // must not be asked for an offset outside the representation. + auto res = cli_.Get("/streamed-with-range-and-error-status", + Headers{{make_range_header({{3, 5}})}}); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::Forbidden_403, res->status); + EXPECT_EQ("7", res->get_header_value("Content-Length")); + EXPECT_EQ(false, res->has_header("Content-Range")); + EXPECT_EQ(std::string("abcdefg"), res->body); + + auto res2 = cli_.Get("/streamed-with-range-and-error-status", + Headers{{"Range", "bytes=100000-100200"}}); + ASSERT_TRUE(res2) << "Error: " << to_string(res2.error()); + EXPECT_EQ(StatusCode::Forbidden_403, res2->status); + EXPECT_EQ("7", res2->get_header_value("Content-Length")); + EXPECT_EQ(std::string("abcdefg"), res2->body); +} + TEST_F(ServerTest, GetStreamedWithRangeError) { auto res = cli_.Get("/streamed-with-range",