Fixed libcurl connection pool to use `Connection` response header values (#5473)

* Fixed libcurl connection pool to use `Connection` response header values

---------

Co-authored-by: Anton Kolesnyk <antkmsft@users.noreply.github.com>
This commit is contained in:
Anton Kolesnyk 2024-04-10 08:01:29 -07:00 коммит произвёл GitHub
Родитель 936f618d3d
Коммит 145f6dbdb6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
6 изменённых файлов: 133 добавлений и 22 удалений

Просмотреть файл

@ -1,14 +1,10 @@
# Release History
## 1.12.0-beta.1 (Unreleased)
### Features Added
### Breaking Changes
## 1.12.0-beta.1 (2024-04-10)
### Bugs Fixed
### Other Changes
- [[#5450]](https://github.com/Azure/azure-sdk-for-cpp/issues/5450) Fixed libcurl connection pool to use `Connection` response header values.
## 1.11.3 (2024-04-09)

Просмотреть файл

@ -138,6 +138,18 @@ namespace Azure { namespace Core { namespace Http {
// adding getters for version and stream body. Clang will complain on macOS if we have unused
// fields in a class
/**
* @brief Get HTTP protocol major version.
*
*/
int32_t GetMajorVersion() const { return m_majorVersion; }
/**
* @brief Get HTTP protocol minor version.
*
*/
int32_t GetMinorVersion() const { return m_minorVersion; }
/**
* @brief Get HTTP status code of the HTTP response.
*

Просмотреть файл

@ -874,6 +874,100 @@ CURLcode CurlSession::ReadStatusLineAndHeadersFromRawResponse(
this->m_innerBufferSize = bufferSize;
this->m_lastStatusCode = this->m_response->GetStatusCode();
// The logic below comes from the expectation that Azure services, particularly Storage, may not
// conform to HTTP standards when it comes to handling 100-continue requests, and not send
// "Connection: close" when they should. We do not know for sure if this is true, but this logic
// did exist for libcurl transport in earlier C++ SDK versions.
//
// The idea is the following: if status code is not 2xx, and request header contains "Expect:
// 100-continue" and request body length is not zero, we don't reuse the connection.
//
// More detailed description of what might happen if we don't have this logic:
// 1. Storage SDK sends a PUT request with a non-empty request body (which means Content-Length
// request header is not 0, let's say it's 6) and Expect: 100-continue request header, but it
// doesn't send the header unless server returns 100 Continue status code.
// 2. Storage service returns 4xx status code and response headers, but it doesn't want to close
// this connection, so there's no Connection: close in response headers.
// 3. Now both client and server agree to continue using this connection. But they do not agree in
// the current status of this connection.
// 3.1. Client side thinks the previous request is finished because it has received a status
// code and response headers. It should send a new HTTP request if there's any.
// 3.2. Server side thinks the previous request is not finished because it hasn't received the
// request body. I tend to think this is a bug of server-side.
// 4. Client side sends a new request, for example,
// HEAD /whatever/path HTTP/1.1
// host: foo.bar.com
// ...
// 5. Server side takes the first 6 bytes (HEAD /) of the send request and thinks this is the
// request body of the first request and discard it.
// 6. Server side keeps reading the remaining data on the wire and thinks the first part
// (whatever/path) is an HTTP verb. It fails the request with 400 invalid verb.
bool non2xxAfter100ContinueWithNonzeroContentLength = false;
{
auto responseHttpCodeInt
= static_cast<std::underlying_type<Http::HttpStatusCode>::type>(m_lastStatusCode);
if (responseHttpCodeInt < 200 || responseHttpCodeInt >= 300)
{
const auto requestExpectHeader = m_request.GetHeader("Expect");
if (requestExpectHeader.HasValue())
{
const auto requestExpectHeaderValueLowercase
= Core::_internal::StringExtensions::ToLower(requestExpectHeader.Value());
if (requestExpectHeaderValueLowercase == "100-continue")
{
const auto requestContentLengthHeaderValue = m_request.GetHeader("Content-Length");
if (requestContentLengthHeaderValue.HasValue()
&& requestContentLengthHeaderValue.Value() != "0")
{
non2xxAfter100ContinueWithNonzeroContentLength = true;
}
}
}
}
}
if (non2xxAfter100ContinueWithNonzeroContentLength)
{
m_httpKeepAlive = false;
}
else
{
bool hasConnectionKeepAlive = false;
bool hasConnectionClose = false;
{
const Core::CaseInsensitiveMap& responseHeaders = m_response->GetHeaders();
const auto connectionHeader = responseHeaders.find("Connection");
if (connectionHeader != responseHeaders.cend())
{
const std::string headerValueLowercase
= Core::_internal::StringExtensions::ToLower(connectionHeader->second);
hasConnectionKeepAlive = headerValueLowercase.find("keep-alive") != std::string::npos;
hasConnectionClose = headerValueLowercase.find("close") != std::string::npos;
}
}
// HTTP <=1.0 is "close" by default. HTTP 1.1 is "keep-alive" by default.
// The value can also be "keep-alive, close" (i.e. "both are fine"), in which case we are
// preferring to treat it as keep-alive.
// (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection)
// Should it come to HTTP/2 and HTTP/3, they are "keep-alive", but any response from HTTP/2 or
// /3 containing a "Connection" header should be considered malformed. (HTTP/2:
// https://httpwg.org/specs/rfc9113.html#ConnectionSpecific
// HTTP/3: https://httpwg.org/specs/rfc9114.html#rfc.section.4.2)
if (m_response->GetMajorVersion() == 1 && m_response->GetMinorVersion() >= 1)
{
m_httpKeepAlive = (!hasConnectionClose || hasConnectionKeepAlive);
}
else if (m_response->GetMajorVersion() <= 1)
{
m_httpKeepAlive = hasConnectionKeepAlive;
}
else
{
m_httpKeepAlive = true;
}
}
// For Head request, set the length of body response to 0.
// Response will give us content-length as if we were not doing Head saying what would it be the
// length of the body. However, Server won't send body
@ -2129,14 +2223,11 @@ std::unique_ptr<CurlNetworkConnection> CurlConnectionPool::ExtractOrCreateCurlCo
// first connection to be picked next time some one ask for a connection to the pool (LIFO)
void CurlConnectionPool::MoveConnectionBackToPool(
std::unique_ptr<CurlNetworkConnection> connection,
HttpStatusCode lastStatusCode)
bool httpKeepAlive)
{
auto code = static_cast<std::underlying_type<Http::HttpStatusCode>::type>(lastStatusCode);
// laststatusCode = 0
if (code < 200 || code >= 300)
if (!httpKeepAlive)
{
// A handler with previous response with Error can't be re-use.
return;
return; // The server has asked us to not re-use this connection.
}
if (connection->IsShutdown())

Просмотреть файл

@ -91,11 +91,12 @@ namespace Azure { namespace Core { namespace Http { namespace _detail {
* @brief Moves a connection back to the pool to be re-used.
*
* @param connection CURL HTTP connection to add to the pool.
* @param lastStatusCode The most recent HTTP status code received from the \p connection.
* @param httpKeepAlive The status of keep-alive behavior, based on HTTP protocol version and
* the most recent response header received through the \p connection.
*/
void MoveConnectionBackToPool(
std::unique_ptr<CurlNetworkConnection> connection,
HttpStatusCode lastStatusCode);
bool httpKeepAlive);
/**
* @brief Keeps a unique key for each host and creates a connection pool for each key.

Просмотреть файл

@ -339,6 +339,13 @@ namespace Azure { namespace Core { namespace Http {
*/
Http::HttpStatusCode m_lastStatusCode = Http::HttpStatusCode::BadRequest;
/**
* @brief Holds information on whether the connection can be kept alive, based on HTTP protocol
* version and the "Connection" HTTP header.
*
*/
bool m_httpKeepAlive = false;
/**
* @brief check whether an end of file has been reached.
* @return `true` if end of file has been reached; otherwise, `false`.
@ -417,7 +424,7 @@ namespace Azure { namespace Core { namespace Http {
if (IsEOF() && m_keepAlive && !m_connectionUpgraded)
{
_detail::CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool(
std::move(m_connection), m_lastStatusCode);
std::move(m_connection), m_httpKeepAlive);
}
}

Просмотреть файл

@ -73,6 +73,7 @@ namespace Azure { namespace Core { namespace Test {
// Simulate connection was used already
session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok;
session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING;
session->m_httpKeepAlive = true;
}
// Check that after the connection is gone, it is moved back to the pool
{
@ -110,6 +111,7 @@ namespace Azure { namespace Core { namespace Test {
// Simulate connection was used already
session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok;
session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING;
session->m_httpKeepAlive = true;
}
{
std::lock_guard<std::mutex> lock(
@ -155,6 +157,7 @@ namespace Azure { namespace Core { namespace Test {
// Simulate connection was used already
session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok;
session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING;
session->m_httpKeepAlive = true;
}
// Now there should be 2 index wit one connection each
@ -219,6 +222,7 @@ namespace Azure { namespace Core { namespace Test {
// Simulate connection was used already
session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok;
session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING;
session->m_httpKeepAlive = true;
}
// Now there should be 2 index wit one connection each
EXPECT_EQ(
@ -282,7 +286,7 @@ namespace Azure { namespace Core { namespace Test {
for (int count = 0; count < 5; count++)
{
CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool(
std::move(connections[count]), Http::HttpStatusCode::Ok);
std::move(connections[count]), true);
}
}
@ -351,7 +355,7 @@ namespace Azure { namespace Core { namespace Test {
// CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool(
// std::unique_ptr<MockCurlNetworkConnection>(curlMock),
// Azure::Core::Http::HttpStatusCode::Ok);
// true);
// }
// // No need to take look here because connections are mocked to never be expired.
// EXPECT_EQ(
@ -389,7 +393,7 @@ namespace Azure { namespace Core { namespace Test {
// CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool(
// std::unique_ptr<MockCurlNetworkConnection>(curlMock),
// Azure::Core::Http::HttpStatusCode::Ok);
// true);
// EXPECT_EQ(
// Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
@ -455,7 +459,7 @@ namespace Azure { namespace Core { namespace Test {
}
// move connection back to the pool
Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
.MoveConnectionBackToPool(std::move(connection), Azure::Core::Http::HttpStatusCode::Ok);
.MoveConnectionBackToPool(std::move(connection), true);
}
{
@ -494,7 +498,7 @@ namespace Azure { namespace Core { namespace Test {
}
// move connection back to the pool
Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
.MoveConnectionBackToPool(std::move(connection), Azure::Core::Http::HttpStatusCode::Ok);
.MoveConnectionBackToPool(std::move(connection), true);
}
{
std::lock_guard<std::mutex> lock(
@ -532,7 +536,7 @@ namespace Azure { namespace Core { namespace Test {
EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey);
// move connection back to the pool
Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
.MoveConnectionBackToPool(std::move(connection), Azure::Core::Http::HttpStatusCode::Ok);
.MoveConnectionBackToPool(std::move(connection), true);
}
{
@ -570,7 +574,7 @@ namespace Azure { namespace Core { namespace Test {
}
// move connection back to the pool
Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
.MoveConnectionBackToPool(std::move(connection), Azure::Core::Http::HttpStatusCode::Ok);
.MoveConnectionBackToPool(std::move(connection), true);
}
{
std::lock_guard<std::mutex> lock(