@@ -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)
21302224void 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 ())
0 commit comments