mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-01 05:02:29 +07:00
Match the Expect 100-continue expectation as a token
RFC 9110 Section 10.1.1 defines Expect as a comma-separated list, states that its value is case-insensitive, and requires a server that receives a 100-continue expectation in an HTTP/1.0 request to ignore it. Comparing the whole field value against "100-continue" met none of those. An HTTP/1.0 request asking for 100-continue was answered with a 100 (Continue) interim response, which that section forbids. "100-Continue" and "100-continue, foo" were both read as no expectation at all, so a client that waits for the interim response before sending its content waited for a response that was never coming. Route the check through has_header_token(), which walks every field line and compares complete tokens case-insensitively, and skip it for HTTP/1.0. An expectation cpp-httplib does not recognize is still ignored rather than refused; the 417 the section offers for one is a MAY, not a requirement.
This commit is contained in:
@@ -13519,7 +13519,12 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
|
||||
if (setup_request) { setup_request(req); }
|
||||
|
||||
if (req.get_header_value("Expect") == "100-continue") {
|
||||
// RFC 9110 10.1.1: Expect is a comma-separated list whose value is
|
||||
// case-insensitive, and a 100-continue expectation in an HTTP/1.0 request
|
||||
// must be ignored. An expectation we do not recognize is left alone; the
|
||||
// 417 the section allows for one is a MAY, not a requirement.
|
||||
if (req.version != "HTTP/1.0" &&
|
||||
detail::has_header_token(req.headers, "Expect", "100-continue")) {
|
||||
int status = StatusCode::Continue_100;
|
||||
if (expect_100_continue_handler_) {
|
||||
status = expect_100_continue_handler_(req, res);
|
||||
@@ -15082,7 +15087,8 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
}
|
||||
|
||||
// Check for Expect: 100-continue
|
||||
auto expect_100_continue = req.get_header_value("Expect") == "100-continue";
|
||||
auto expect_100_continue =
|
||||
detail::has_header_token(req.headers, "Expect", "100-continue");
|
||||
|
||||
// Send request (skip body if using Expect: 100-continue)
|
||||
auto write_request_success =
|
||||
|
||||
@@ -16111,6 +16111,93 @@ TEST(InvalidHeaderValueTest, InvalidContentLength) {
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
// RFC 9110 Section 10.1.1: Expect is a comma-separated list, its value is
|
||||
// case-insensitive, and a server that receives a 100-continue expectation in
|
||||
// an HTTP/1.0 request MUST ignore it.
|
||||
static void probe_expect(int port, const std::string &req, bool *got_100) {
|
||||
auto error = Error::Success;
|
||||
auto sock = detail::create_client_socket(
|
||||
HOST, "", port, AF_UNSPEC, false, false, nullptr,
|
||||
/*connection_timeout_sec=*/5, 0,
|
||||
/*read_timeout_sec=*/1, 0,
|
||||
/*write_timeout_sec=*/5, 0, std::string(), error);
|
||||
ASSERT_NE(sock, INVALID_SOCKET);
|
||||
auto se = detail::scope_exit([&] { detail::close_socket(sock); });
|
||||
|
||||
std::string first_line;
|
||||
detail::process_client_socket(
|
||||
sock, 1, 0, 5, 0, 0, std::chrono::steady_clock::time_point::min(),
|
||||
[&](Stream &strm) {
|
||||
if (strm.write(req.data(), req.size()) !=
|
||||
static_cast<ssize_t>(req.size())) {
|
||||
return false;
|
||||
}
|
||||
char buf[512];
|
||||
detail::stream_line_reader reader(strm, buf, sizeof(buf));
|
||||
if (reader.getline()) { first_line = reader.ptr(); }
|
||||
return true;
|
||||
});
|
||||
|
||||
*got_100 = first_line.find("100 Continue") != std::string::npos;
|
||||
}
|
||||
|
||||
class ExpectTokenTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
svr_.Post("/p", [](const Request &, Response &res) {
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
port_ = svr_.bind_to_any_port(HOST);
|
||||
thread_ = thread([&]() { svr_.listen_after_bind(); });
|
||||
svr_.wait_until_ready();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
svr_.stop();
|
||||
if (thread_.joinable()) { thread_.join(); }
|
||||
}
|
||||
|
||||
std::string request(const char *version, const char *expect_value) const {
|
||||
return std::string("POST /p ") + version +
|
||||
"\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Content-Length: 2\r\n"
|
||||
"Expect: " +
|
||||
expect_value +
|
||||
"\r\n"
|
||||
"\r\n"
|
||||
"hi";
|
||||
}
|
||||
|
||||
Server svr_;
|
||||
int port_ = 0;
|
||||
thread thread_;
|
||||
};
|
||||
|
||||
TEST_F(ExpectTokenTest, Http11GetsTheInterimResponse) {
|
||||
bool got_100 = false;
|
||||
probe_expect(port_, request("HTTP/1.1", "100-continue"), &got_100);
|
||||
EXPECT_TRUE(got_100);
|
||||
}
|
||||
|
||||
TEST_F(ExpectTokenTest, Http10ExpectationIsIgnored) {
|
||||
bool got_100 = true;
|
||||
probe_expect(port_, request("HTTP/1.0", "100-continue"), &got_100);
|
||||
EXPECT_FALSE(got_100);
|
||||
}
|
||||
|
||||
TEST_F(ExpectTokenTest, ExpectationIsCaseInsensitive) {
|
||||
bool got_100 = false;
|
||||
probe_expect(port_, request("HTTP/1.1", "100-Continue"), &got_100);
|
||||
EXPECT_TRUE(got_100);
|
||||
}
|
||||
|
||||
TEST_F(ExpectTokenTest, ExpectationAmongOthersIsRecognized) {
|
||||
bool got_100 = false;
|
||||
probe_expect(port_, request("HTTP/1.1", "100-continue, foo"), &got_100);
|
||||
EXPECT_TRUE(got_100);
|
||||
}
|
||||
|
||||
TEST(Expect100ContinueTest, ServerClosesConnection) {
|
||||
static constexpr char reject[] = "Unauthorized";
|
||||
static constexpr char accept[] = "Upload accepted";
|
||||
|
||||
Reference in New Issue
Block a user