diff --git a/httplib.h b/httplib.h index b7b2104e..f1da3510 100644 --- a/httplib.h +++ b/httplib.h @@ -15280,9 +15280,13 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, return req.method == "CONNECT"; } - // Ignore '100 Continue' (only when not using Expect: 100-continue explicitly) - while (skip_100_continue && res.status == StatusCode::Continue_100) { - if (!line_reader.getline()) { return false; } // CRLF + // Skip interim responses, such as 100 Continue (unless Expect: + // 100-continue is handled by the caller) and 103 Early Hints, along with + // their header fields. 101 ends HTTP on the connection, so it is final. + while (skip_100_continue && 100 <= res.status && res.status < 200 && + res.status != StatusCode::SwitchingProtocol_101) { + Headers interim_headers; + if (!detail::read_headers(strm, interim_headers)) { return false; } if (!line_reader.getline()) { return false; } // next response line if (!detail::parse_status_line(line_reader.ptr(), res.version, res.status, diff --git a/test/test.cc b/test/test.cc index 061823b1..eefa75fe 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13535,6 +13535,66 @@ TEST(ClientPayloadTest, DoesNotReserveTheClaimedLength) { EXPECT_FALSE(cli.Get("/")); } +// Accepts one connection on 127.0.0.1:port, reads the request head and sends +// back the given bytes, which httplib::Server could not be made to send. +static void serve_raw_once(int port, const std::string &response) { + auto srv = ::socket(AF_INET, SOCK_STREAM, 0); + default_socket_options(srv); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(static_cast(port)); + ::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + int opt = 1; + ::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, +#ifdef _WIN32 + reinterpret_cast(&opt), +#else + &opt, +#endif + sizeof(opt)); + ::bind(srv, reinterpret_cast(&addr), sizeof(addr)); + ::listen(srv, 1); + + auto cli = ::accept(srv, nullptr, nullptr); + if (cli != INVALID_SOCKET) { + detail::set_socket_opt_time(cli, SOL_SOCKET, SO_RCVTIMEO, 3, 0); + std::string head; + char c; + while (head.find("\r\n\r\n") == std::string::npos && + ::recv(cli, &c, 1, 0) == 1) { + head += c; + } + ::send(cli, response.data(), static_cast(response.size()), 0); + detail::close_socket(cli); + } + detail::close_socket(srv); +} + +TEST(InterimResponseTest, SkippedWithTheirHeaderFields) { + const char *responses[] = { + "HTTP/1.1 103 Early Hints\r\n" + "Link: ; rel=preload\r\n" + "\r\n" + "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok", + "HTTP/1.1 100 Continue\r\n" + "X-Note: interim\r\n" + "\r\n" + "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok", + }; + + for (auto response : responses) { + std::thread t([&]() { serve_raw_once(PORT, response); }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + Client cli("127.0.0.1", PORT); + auto res = cli.Get("/"); + t.join(); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ("ok", res->body); + } +} + TEST(ExpectContinueTest, EarlyFinalResponseClosesConnection) { std::atomic closed_by_client{false}; auto server_thread = std::thread([&] {