Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": true
},
"ghcr.io/devcontainers/features/dotnet:2.3.0": {
"version": "9.0",
"ghcr.io/devcontainers/features/dotnet:2.4.1": {
"version": "10.0",
"installUsingApt": false
}
},
Expand Down
41 changes: 27 additions & 14 deletions src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,28 +147,41 @@ private HttpResponseMessage CreateResponseMessage(List<string> responseLines)
}
}

// TODO: We'll need to refactor this in the future.
// Response handling is based on headers and type. The implementation currently covers
// four main cases:
//
// Depending on the request and response (headers), we need to handle the response
// differently. We need to distinguish between four types of responses:
// 1. Chunked transfer encoding (HTTP/1.1 `Transfer-Encoding: chunked`)
// 2. HTTP responses with a `Content-Length` header
// - For 101 Switching Protocols, `Content-Length` is ignored per RFC 9110
// 3. Protocol upgrades (HTTP 101 + `Upgrade: tcp`)
// - e.g., `/containers/{id}/attach` or `/exec/{id}/start`
// 4. Streaming responses without connection upgrade headers
// - e.g., `/containers/{id}/logs`
//
// 1. Chunked transfer encoding
// 2. HTTP with a `Content-Length` header
// 3. Hijacked TCP connections (using the connection upgrade headers)
// - `/containers/{id}/attach`
// - `/exec/{id}/start`
// 4. Streams without the connection upgrade headers
// - `/containers/{id}/logs`
// This separation ensures chunked framing, streaming, and upgraded connections are handled
// correctly, while tolerating proxies that incorrectly send `Content-Length: 0` on upgrades.

var isSwitchingProtocols = response.StatusCode == HttpStatusCode.SwitchingProtocols;

var isConnectionUpgrade = response.Headers.TryGetValues("Upgrade", out var responseHeaderValues)
&& responseHeaderValues.Any(header => "tcp".Equals(header));
&& responseHeaderValues.Any(header => "tcp".Equals(header, StringComparison.OrdinalIgnoreCase));

var isStream = content.Headers.TryGetValues("Content-Type", out var contentHeaderValues)
&& contentHeaderValues.Any(header => DockerStreamHeaders.Contains(header));
&& contentHeaderValues.Any(DockerStreamHeaders.Contains);

var isChunkedTransferEncoding = (response.Headers.TransferEncodingChunked.GetValueOrDefault() && !isStream) || (isStream && !isConnectionUpgrade);
// Treat the response as chunked for standard HTTP chunked or Docker raw-streams,
// but not for upgraded connections.
var isChunkedTransferEncoding = (response.Headers.TransferEncodingChunked.GetValueOrDefault() && !isConnectionUpgrade)
|| (!isConnectionUpgrade && isStream);

content.ResolveResponseStream(chunked: isChunkedTransferEncoding);
if (isSwitchingProtocols && isConnectionUpgrade)
{
content.ResolveResponseStream(false, true);
}
else
{
content.ResolveResponseStream(isChunkedTransferEncoding, isConnectionUpgrade);
}

return response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal HttpConnectionResponseContent(HttpConnection connection)
_connection = connection;
}

internal void ResolveResponseStream(bool chunked)
internal void ResolveResponseStream(bool chunked, bool isConnectionUpgrade)
{
if (_responseStream != null)
{
Expand All @@ -20,7 +20,7 @@ internal void ResolveResponseStream(bool chunked)
{
_responseStream = new ChunkedReadStream(_connection.Transport);
}
else if (Headers.ContentLength.HasValue)
else if (!isConnectionUpgrade && Headers.ContentLength.HasValue)
{
_responseStream = new ContentLengthReadStream(_connection.Transport, Headers.ContentLength.Value);
}
Expand Down
Loading