Skip to content

Commit 5021303

Browse files
vadzras0219-msft
authored andcommitted
Fix handling of timed out connections kept alive in connection pool under Unix (#762)
* Add asio_connection::was_closed_by_server() to reduce duplication Reduce code duplication between ssl_proxy_tunnel::handle_status_line() and the method with the same name in asio_context itself by moving the common handling of connections closed by server into a new function. No real changes, this is a pure refactoring. * Fix checking for server-side closure of HTTPS connections When an SSL connection times out due to being closed by server, a different error code is returned, so we need to check for it too in was_closed_by_server(). Without this, losing connection was not detected at all when using HTTPS, resulting in "Failed to read HTTP status line" errors whenever the same http_client was reused after more than the server keep alive timeout of inactivity. See #592. * Fix bug with using re-opened connections with ASIO Creating a new request when the existing connection being used was closed by the server didn't work correctly because the associated input stream was already exhausted, as its contents had been already "sent" to the server using the now discarded connection, so starting a new request using the same body stream never worked. Fix this by explicitly rewinding the stream to the beginning before using it again. Note that even with this fix using a custom body stream which is not positioned at the beginning initially (or which doesn't support rewinding) still wouldn't work, but at least it fixes the most common use case. See #592. * Reduce duplicate code between ssl_proxy and asio_context in handle_read_status_line. Avoid increasing public surface with rewind function.
1 parent c8c9227 commit 5021303

File tree

1 file changed

+74
-52
lines changed

1 file changed

+74
-52
lines changed

Release/src/http/client/http_client_asio.cpp

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#endif
2929
#include <boost/asio.hpp>
3030
#include <boost/asio/ssl.hpp>
31+
#include <boost/asio/ssl/error.hpp>
3132
#include <boost/asio/steady_timer.hpp>
3233
#include <boost/algorithm/string.hpp>
3334
#include <boost/bind.hpp>
@@ -170,6 +171,45 @@ class asio_connection
170171
bool keep_alive() const { return m_keep_alive; }
171172
bool is_ssl() const { return m_ssl_stream ? true : false; }
172173

174+
// Check if the error code indicates that the connection was closed by the
175+
// server: this is used to detect if a connection in the pool was closed during
176+
// its period of inactivity and we should reopen it.
177+
bool was_reused_and_closed_by_server(const boost::system::error_code& ec) const
178+
{
179+
if (!is_reused())
180+
{
181+
// Don't bother reopening the connection if it's a new one: in this
182+
// case, even if the connection was really lost, it's still a real
183+
// error and we shouldn't try to reopen it.
184+
return false;
185+
}
186+
187+
// These errors tell if connection was closed.
188+
if ((boost::asio::error::eof == ec)
189+
|| (boost::asio::error::connection_reset == ec)
190+
|| (boost::asio::error::connection_aborted == ec))
191+
{
192+
return true;
193+
}
194+
195+
if (is_ssl())
196+
{
197+
// For SSL connections, we can also get a different error due to
198+
// incorrect secure connection shutdown if it was closed by the
199+
// server due to inactivity. Unfortunately, the exact error we get
200+
// in this case depends on the Boost.Asio version used.
201+
#if BOOST_ASIO_VERSION >= 101008
202+
if (boost::asio::ssl::error::stream_truncated == ec)
203+
return true;
204+
#else // Asio < 1.10.8 didn't have ssl::error::stream_truncated
205+
if (boost::system::error_code(ERR_PACK(ERR_LIB_SSL, 0, SSL_R_SHORT_READ), boost::asio::error::get_ssl_category()) == ec)
206+
return true;
207+
#endif
208+
}
209+
210+
return false;
211+
}
212+
173213
template <typename Iterator, typename Handler>
174214
void async_connect(const Iterator &begin, const Handler &handler)
175215
{
@@ -578,37 +618,13 @@ class asio_context : public request_context, public std::enable_shared_from_this
578618
return;
579619
}
580620

581-
m_context->m_connection->upgrade_to_ssl(m_context->m_http_client->client_config().get_ssl_context_callback());
621+
m_context->upgrade_to_ssl();
582622

583623
m_ssl_tunnel_established(m_context);
584624
}
585625
else
586626
{
587-
// These errors tell if connection was closed.
588-
const bool socket_was_closed((boost::asio::error::eof == ec)
589-
|| (boost::asio::error::connection_reset == ec)
590-
|| (boost::asio::error::connection_aborted == ec));
591-
if (socket_was_closed && m_context->m_connection->is_reused())
592-
{
593-
// Failed to write to socket because connection was already closed while it was in the pool.
594-
// close() here ensures socket is closed in a robust way and prevents the connection from being put to the pool again.
595-
m_context->m_connection->close();
596-
597-
// Create a new context and copy the request object, completion event and
598-
// cancellation registration to maintain the old state.
599-
// This also obtains a new connection from pool.
600-
auto new_ctx = m_context->create_request_context(m_context->m_http_client, m_context->m_request);
601-
new_ctx->m_request_completion = m_context->m_request_completion;
602-
new_ctx->m_cancellationRegistration = m_context->m_cancellationRegistration;
603-
604-
auto client = std::static_pointer_cast<asio_client>(m_context->m_http_client);
605-
// Resend the request using the new context.
606-
client->send_request(new_ctx);
607-
}
608-
else
609-
{
610-
m_context->report_error("Failed to read HTTP status line from proxy", ec, httpclient_errorcode_context::readheader);
611-
}
627+
m_context->handle_failed_read_status_line(ec, "Failed to read HTTP status line from proxy");
612628
}
613629
}
614630

@@ -619,7 +635,6 @@ class asio_context : public request_context, public std::enable_shared_from_this
619635
boost::asio::streambuf m_response;
620636
};
621637

622-
623638
enum class http_proxy_type
624639
{
625640
none,
@@ -821,6 +836,11 @@ class asio_context : public request_context, public std::enable_shared_from_this
821836
}
822837

823838
private:
839+
void upgrade_to_ssl()
840+
{
841+
m_connection->upgrade_to_ssl(m_http_client->client_config().get_ssl_context_callback());
842+
}
843+
824844
std::string generate_basic_auth_header()
825845
{
826846
std::string header;
@@ -1173,31 +1193,33 @@ class asio_context : public request_context, public std::enable_shared_from_this
11731193
}
11741194
else
11751195
{
1176-
// These errors tell if connection was closed.
1177-
const bool socket_was_closed((boost::asio::error::eof == ec)
1178-
|| (boost::asio::error::connection_reset == ec)
1179-
|| (boost::asio::error::connection_aborted == ec));
1180-
if (socket_was_closed && m_connection->is_reused())
1181-
{
1182-
// Failed to write to socket because connection was already closed while it was in the pool.
1183-
// close() here ensures socket is closed in a robust way and prevents the connection from being put to the pool again.
1184-
m_connection->close();
1185-
1186-
// Create a new context and copy the request object, completion event and
1187-
// cancellation registration to maintain the old state.
1188-
// This also obtains a new connection from pool.
1189-
auto new_ctx = create_request_context(m_http_client, m_request);
1190-
new_ctx->m_request_completion = m_request_completion;
1191-
new_ctx->m_cancellationRegistration = m_cancellationRegistration;
1192-
1193-
auto client = std::static_pointer_cast<asio_client>(m_http_client);
1194-
// Resend the request using the new context.
1195-
client->send_request(new_ctx);
1196-
}
1197-
else
1198-
{
1199-
report_error("Failed to read HTTP status line", ec, httpclient_errorcode_context::readheader);
1200-
}
1196+
handle_failed_read_status_line(ec, "Failed to read HTTP status line");
1197+
}
1198+
}
1199+
1200+
void handle_failed_read_status_line(const boost::system::error_code& ec, const char* generic_error_message)
1201+
{
1202+
if (m_connection->was_reused_and_closed_by_server(ec))
1203+
{
1204+
// Failed to write to socket because connection was already closed while it was in the pool.
1205+
// close() here ensures socket is closed in a robust way and prevents the connection from being put to the pool again.
1206+
m_connection->close();
1207+
1208+
// Create a new context and copy the request object, completion event and
1209+
// cancellation registration to maintain the old state.
1210+
// This also obtains a new connection from pool.
1211+
auto new_ctx = create_request_context(m_http_client, m_request);
1212+
new_ctx->m_request._get_impl()->instream().seek(0);
1213+
new_ctx->m_request_completion = m_request_completion;
1214+
new_ctx->m_cancellationRegistration = m_cancellationRegistration;
1215+
1216+
auto client = std::static_pointer_cast<asio_client>(m_http_client);
1217+
// Resend the request using the new context.
1218+
client->send_request(new_ctx);
1219+
}
1220+
else
1221+
{
1222+
report_error(generic_error_message, ec, httpclient_errorcode_context::readheader);
12011223
}
12021224
}
12031225

0 commit comments

Comments
 (0)