Skip to content

Commit 145f6db

Browse files
authored
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>
1 parent 936f618 commit 145f6db

File tree

6 files changed

+133
-22
lines changed

6 files changed

+133
-22
lines changed

sdk/core/azure-core/CHANGELOG.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
# Release History
22

3-
## 1.12.0-beta.1 (Unreleased)
4-
5-
### Features Added
6-
7-
### Breaking Changes
3+
## 1.12.0-beta.1 (2024-04-10)
84

95
### Bugs Fixed
106

11-
### Other Changes
7+
- [[#5450]](https://github.com/Azure/azure-sdk-for-cpp/issues/5450) Fixed libcurl connection pool to use `Connection` response header values.
128

139
## 1.11.3 (2024-04-09)
1410

sdk/core/azure-core/inc/azure/core/http/raw_response.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,18 @@ namespace Azure { namespace Core { namespace Http {
138138
// adding getters for version and stream body. Clang will complain on macOS if we have unused
139139
// fields in a class
140140

141+
/**
142+
* @brief Get HTTP protocol major version.
143+
*
144+
*/
145+
int32_t GetMajorVersion() const { return m_majorVersion; }
146+
147+
/**
148+
* @brief Get HTTP protocol minor version.
149+
*
150+
*/
151+
int32_t GetMinorVersion() const { return m_minorVersion; }
152+
141153
/**
142154
* @brief Get HTTP status code of the HTTP response.
143155
*

sdk/core/azure-core/src/http/curl/curl.cpp

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,100 @@ CURLcode CurlSession::ReadStatusLineAndHeadersFromRawResponse(
874874
this->m_innerBufferSize = bufferSize;
875875
this->m_lastStatusCode = this->m_response->GetStatusCode();
876876

877+
// The logic below comes from the expectation that Azure services, particularly Storage, may not
878+
// conform to HTTP standards when it comes to handling 100-continue requests, and not send
879+
// "Connection: close" when they should. We do not know for sure if this is true, but this logic
880+
// did exist for libcurl transport in earlier C++ SDK versions.
881+
//
882+
// The idea is the following: if status code is not 2xx, and request header contains "Expect:
883+
// 100-continue" and request body length is not zero, we don't reuse the connection.
884+
//
885+
// More detailed description of what might happen if we don't have this logic:
886+
// 1. Storage SDK sends a PUT request with a non-empty request body (which means Content-Length
887+
// request header is not 0, let's say it's 6) and Expect: 100-continue request header, but it
888+
// doesn't send the header unless server returns 100 Continue status code.
889+
// 2. Storage service returns 4xx status code and response headers, but it doesn't want to close
890+
// this connection, so there's no Connection: close in response headers.
891+
// 3. Now both client and server agree to continue using this connection. But they do not agree in
892+
// the current status of this connection.
893+
// 3.1. Client side thinks the previous request is finished because it has received a status
894+
// code and response headers. It should send a new HTTP request if there's any.
895+
// 3.2. Server side thinks the previous request is not finished because it hasn't received the
896+
// request body. I tend to think this is a bug of server-side.
897+
// 4. Client side sends a new request, for example,
898+
// HEAD /whatever/path HTTP/1.1
899+
// host: foo.bar.com
900+
// ...
901+
// 5. Server side takes the first 6 bytes (HEAD /) of the send request and thinks this is the
902+
// request body of the first request and discard it.
903+
// 6. Server side keeps reading the remaining data on the wire and thinks the first part
904+
// (whatever/path) is an HTTP verb. It fails the request with 400 invalid verb.
905+
bool non2xxAfter100ContinueWithNonzeroContentLength = false;
906+
{
907+
auto responseHttpCodeInt
908+
= static_cast<std::underlying_type<Http::HttpStatusCode>::type>(m_lastStatusCode);
909+
if (responseHttpCodeInt < 200 || responseHttpCodeInt >= 300)
910+
{
911+
const auto requestExpectHeader = m_request.GetHeader("Expect");
912+
if (requestExpectHeader.HasValue())
913+
{
914+
const auto requestExpectHeaderValueLowercase
915+
= Core::_internal::StringExtensions::ToLower(requestExpectHeader.Value());
916+
if (requestExpectHeaderValueLowercase == "100-continue")
917+
{
918+
const auto requestContentLengthHeaderValue = m_request.GetHeader("Content-Length");
919+
if (requestContentLengthHeaderValue.HasValue()
920+
&& requestContentLengthHeaderValue.Value() != "0")
921+
{
922+
non2xxAfter100ContinueWithNonzeroContentLength = true;
923+
}
924+
}
925+
}
926+
}
927+
}
928+
929+
if (non2xxAfter100ContinueWithNonzeroContentLength)
930+
{
931+
m_httpKeepAlive = false;
932+
}
933+
else
934+
{
935+
bool hasConnectionKeepAlive = false;
936+
bool hasConnectionClose = false;
937+
{
938+
const Core::CaseInsensitiveMap& responseHeaders = m_response->GetHeaders();
939+
const auto connectionHeader = responseHeaders.find("Connection");
940+
if (connectionHeader != responseHeaders.cend())
941+
{
942+
const std::string headerValueLowercase
943+
= Core::_internal::StringExtensions::ToLower(connectionHeader->second);
944+
hasConnectionKeepAlive = headerValueLowercase.find("keep-alive") != std::string::npos;
945+
hasConnectionClose = headerValueLowercase.find("close") != std::string::npos;
946+
}
947+
}
948+
949+
// HTTP <=1.0 is "close" by default. HTTP 1.1 is "keep-alive" by default.
950+
// The value can also be "keep-alive, close" (i.e. "both are fine"), in which case we are
951+
// preferring to treat it as keep-alive.
952+
// (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection)
953+
// Should it come to HTTP/2 and HTTP/3, they are "keep-alive", but any response from HTTP/2 or
954+
// /3 containing a "Connection" header should be considered malformed. (HTTP/2:
955+
// https://httpwg.org/specs/rfc9113.html#ConnectionSpecific
956+
// HTTP/3: https://httpwg.org/specs/rfc9114.html#rfc.section.4.2)
957+
if (m_response->GetMajorVersion() == 1 && m_response->GetMinorVersion() >= 1)
958+
{
959+
m_httpKeepAlive = (!hasConnectionClose || hasConnectionKeepAlive);
960+
}
961+
else if (m_response->GetMajorVersion() <= 1)
962+
{
963+
m_httpKeepAlive = hasConnectionKeepAlive;
964+
}
965+
else
966+
{
967+
m_httpKeepAlive = true;
968+
}
969+
}
970+
877971
// For Head request, set the length of body response to 0.
878972
// Response will give us content-length as if we were not doing Head saying what would it be the
879973
// length of the body. However, Server won't send body
@@ -2129,14 +2223,11 @@ std::unique_ptr<CurlNetworkConnection> CurlConnectionPool::ExtractOrCreateCurlCo
21292223
// first connection to be picked next time some one ask for a connection to the pool (LIFO)
21302224
void CurlConnectionPool::MoveConnectionBackToPool(
21312225
std::unique_ptr<CurlNetworkConnection> connection,
2132-
HttpStatusCode lastStatusCode)
2226+
bool httpKeepAlive)
21332227
{
2134-
auto code = static_cast<std::underlying_type<Http::HttpStatusCode>::type>(lastStatusCode);
2135-
// laststatusCode = 0
2136-
if (code < 200 || code >= 300)
2228+
if (!httpKeepAlive)
21372229
{
2138-
// A handler with previous response with Error can't be re-use.
2139-
return;
2230+
return; // The server has asked us to not re-use this connection.
21402231
}
21412232

21422233
if (connection->IsShutdown())

sdk/core/azure-core/src/http/curl/curl_connection_pool_private.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,12 @@ namespace Azure { namespace Core { namespace Http { namespace _detail {
9191
* @brief Moves a connection back to the pool to be re-used.
9292
*
9393
* @param connection CURL HTTP connection to add to the pool.
94-
* @param lastStatusCode The most recent HTTP status code received from the \p connection.
94+
* @param httpKeepAlive The status of keep-alive behavior, based on HTTP protocol version and
95+
* the most recent response header received through the \p connection.
9596
*/
9697
void MoveConnectionBackToPool(
9798
std::unique_ptr<CurlNetworkConnection> connection,
98-
HttpStatusCode lastStatusCode);
99+
bool httpKeepAlive);
99100

100101
/**
101102
* @brief Keeps a unique key for each host and creates a connection pool for each key.

sdk/core/azure-core/src/http/curl/curl_session_private.hpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,13 @@ namespace Azure { namespace Core { namespace Http {
339339
*/
340340
Http::HttpStatusCode m_lastStatusCode = Http::HttpStatusCode::BadRequest;
341341

342+
/**
343+
* @brief Holds information on whether the connection can be kept alive, based on HTTP protocol
344+
* version and the "Connection" HTTP header.
345+
*
346+
*/
347+
bool m_httpKeepAlive = false;
348+
342349
/**
343350
* @brief check whether an end of file has been reached.
344351
* @return `true` if end of file has been reached; otherwise, `false`.
@@ -417,7 +424,7 @@ namespace Azure { namespace Core { namespace Http {
417424
if (IsEOF() && m_keepAlive && !m_connectionUpgraded)
418425
{
419426
_detail::CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool(
420-
std::move(m_connection), m_lastStatusCode);
427+
std::move(m_connection), m_httpKeepAlive);
421428
}
422429
}
423430

sdk/core/azure-core/test/ut/curl_connection_pool_test.cpp

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ namespace Azure { namespace Core { namespace Test {
7373
// Simulate connection was used already
7474
session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok;
7575
session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING;
76+
session->m_httpKeepAlive = true;
7677
}
7778
// Check that after the connection is gone, it is moved back to the pool
7879
{
@@ -110,6 +111,7 @@ namespace Azure { namespace Core { namespace Test {
110111
// Simulate connection was used already
111112
session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok;
112113
session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING;
114+
session->m_httpKeepAlive = true;
113115
}
114116
{
115117
std::lock_guard<std::mutex> lock(
@@ -155,6 +157,7 @@ namespace Azure { namespace Core { namespace Test {
155157
// Simulate connection was used already
156158
session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok;
157159
session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING;
160+
session->m_httpKeepAlive = true;
158161
}
159162

160163
// Now there should be 2 index wit one connection each
@@ -219,6 +222,7 @@ namespace Azure { namespace Core { namespace Test {
219222
// Simulate connection was used already
220223
session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok;
221224
session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING;
225+
session->m_httpKeepAlive = true;
222226
}
223227
// Now there should be 2 index wit one connection each
224228
EXPECT_EQ(
@@ -282,7 +286,7 @@ namespace Azure { namespace Core { namespace Test {
282286
for (int count = 0; count < 5; count++)
283287
{
284288
CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool(
285-
std::move(connections[count]), Http::HttpStatusCode::Ok);
289+
std::move(connections[count]), true);
286290
}
287291
}
288292

@@ -351,7 +355,7 @@ namespace Azure { namespace Core { namespace Test {
351355

352356
// CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool(
353357
// std::unique_ptr<MockCurlNetworkConnection>(curlMock),
354-
// Azure::Core::Http::HttpStatusCode::Ok);
358+
// true);
355359
// }
356360
// // No need to take look here because connections are mocked to never be expired.
357361
// EXPECT_EQ(
@@ -389,7 +393,7 @@ namespace Azure { namespace Core { namespace Test {
389393

390394
// CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool(
391395
// std::unique_ptr<MockCurlNetworkConnection>(curlMock),
392-
// Azure::Core::Http::HttpStatusCode::Ok);
396+
// true);
393397

394398
// EXPECT_EQ(
395399
// Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
@@ -455,7 +459,7 @@ namespace Azure { namespace Core { namespace Test {
455459
}
456460
// move connection back to the pool
457461
Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
458-
.MoveConnectionBackToPool(std::move(connection), Azure::Core::Http::HttpStatusCode::Ok);
462+
.MoveConnectionBackToPool(std::move(connection), true);
459463
}
460464

461465
{
@@ -494,7 +498,7 @@ namespace Azure { namespace Core { namespace Test {
494498
}
495499
// move connection back to the pool
496500
Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
497-
.MoveConnectionBackToPool(std::move(connection), Azure::Core::Http::HttpStatusCode::Ok);
501+
.MoveConnectionBackToPool(std::move(connection), true);
498502
}
499503
{
500504
std::lock_guard<std::mutex> lock(
@@ -532,7 +536,7 @@ namespace Azure { namespace Core { namespace Test {
532536
EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey);
533537
// move connection back to the pool
534538
Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
535-
.MoveConnectionBackToPool(std::move(connection), Azure::Core::Http::HttpStatusCode::Ok);
539+
.MoveConnectionBackToPool(std::move(connection), true);
536540
}
537541

538542
{
@@ -570,7 +574,7 @@ namespace Azure { namespace Core { namespace Test {
570574
}
571575
// move connection back to the pool
572576
Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
573-
.MoveConnectionBackToPool(std::move(connection), Azure::Core::Http::HttpStatusCode::Ok);
577+
.MoveConnectionBackToPool(std::move(connection), true);
574578
}
575579
{
576580
std::lock_guard<std::mutex> lock(

0 commit comments

Comments
 (0)