diff --git a/httplib.h b/httplib.h index f529734c..db516502 100644 --- a/httplib.h +++ b/httplib.h @@ -16278,9 +16278,10 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req, // to call it from a different thread since it's a thread-safety issue // to do these things to the socket if another thread is using the socket. std::lock_guard guard(socket_mutex_); - // Also when the request asked for it (e.g. a body that was never sent), - // so that a redirect or an auth retry below does not reuse the socket. - if (socket_should_be_closed_when_request_is_done_ || + // Also when the request asked for it (e.g. a body that was never sent, + // or "Connection: close"), so that a redirect or an auth retry below does + // not reuse the socket. + if (socket_should_be_closed_when_request_is_done_ || close_connection || detail::has_header_token(res.headers, "Connection", "close") || (res.version == "HTTP/1.0" && res.reason != "Connection established")) { disconnect(/*gracefully=*/true); diff --git a/test/test.cc b/test/test.cc index e53aa809..cb7cdef8 100644 --- a/test/test.cc +++ b/test/test.cc @@ -4306,6 +4306,75 @@ TEST(RedirectFromPageWithContent, Redirect) { } } +// A server can close the connection after a request with "Connection: close" +// without repeating the option in its response (RFC 9112 Section 9.6), so the +// redirected request must go out on a new connection. +TEST(RedirectTest, NewConnectionAfterConnectionClose) { +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif + + 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 + 2)); + ::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + ASSERT_EQ(0, ::bind(srv, reinterpret_cast(&addr), sizeof(addr))); + ASSERT_EQ(0, ::listen(srv, 2)); + + auto send_text = [](socket_t sock, const std::string &text) { + ::send(sock, +#ifdef _WIN32 + static_cast(text.c_str()), + static_cast(text.size()), +#else + text.c_str(), text.size(), +#endif + 0); + }; + + auto reused = false; + auto server_thread = std::thread([&] { + char buf[4096]; + auto first = ::accept(srv, nullptr, nullptr); + if (first == INVALID_SOCKET) { return; } + detail::set_socket_opt_time(first, SOL_SOCKET, SO_RCVTIMEO, 5, 0); + ::recv(first, buf, sizeof(buf), 0); + send_text( + first, + "HTTP/1.1 302 Found\r\nLocation: /2\r\nContent-Length: 0\r\n\r\n"); + + auto next = first; + if (::recv(first, buf, sizeof(buf), 0) > 0) { + reused = true; + } else { + detail::close_socket(first); + next = ::accept(srv, nullptr, nullptr); + if (next == INVALID_SOCKET) { return; } + detail::set_socket_opt_time(next, SOL_SOCKET, SO_RCVTIMEO, 5, 0); + ::recv(next, buf, sizeof(buf), 0); + } + send_text(next, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n" + "Connection: close\r\n\r\nok"); + detail::close_socket(next); + }); + + Client cli("127.0.0.1", PORT + 2); + cli.set_follow_location(true); + cli.set_read_timeout(5, 0); + auto res = cli.Get("/1"); + + server_thread.join(); + detail::close_socket(srv); + + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ("ok", res->body); + EXPECT_FALSE(reused); +} + TEST(RedirectFromPageWithContentIP6, Redirect) { Server svr;