< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 270
Coverable lines: 270
Total lines: 460
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 94
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.IO;
 7using System.IO.Compression;
 8using System.Net.Http.Headers;
 9using System.Threading;
 10using System.Threading.Tasks;
 11
 12namespace System.Net.Http
 13{
 14    internal sealed class DecompressionHandler : HttpMessageHandlerStage
 15    {
 16        private readonly HttpMessageHandlerStage _innerHandler;
 17        private readonly DecompressionMethods _decompressionMethods;
 18
 19        private const string Gzip = "gzip";
 20        private const string Deflate = "deflate";
 21        private const string Brotli = "br";
 22        private const string Zstd = "zstd";
 023        private static readonly StringWithQualityHeaderValue s_gzipHeaderValue = new(Gzip);
 024        private static readonly StringWithQualityHeaderValue s_deflateHeaderValue = new(Deflate);
 025        private static readonly StringWithQualityHeaderValue s_brotliHeaderValue = new(Brotli);
 026        private static readonly StringWithQualityHeaderValue s_zstdHeaderValue = new(Zstd);
 27
 28        /// <summary>Header value for all enabled decompression methods, e.g. "gzip, deflate".</summary>
 29        private readonly string _acceptEncodingHeaderValue;
 30
 031        public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessageHandlerStage innerHandler)
 032        {
 033            Debug.Assert(decompressionMethods != DecompressionMethods.None);
 034            Debug.Assert(innerHandler != null);
 35
 036            _decompressionMethods = decompressionMethods;
 037            _innerHandler = innerHandler;
 38
 039            Span<string?> methods = [null, null, null, null];
 040            int count = 0;
 041            if (GZipEnabled) methods[count++] = Gzip;
 042            if (DeflateEnabled) methods[count++] = Deflate;
 043            if (BrotliEnabled) methods[count++] = Brotli;
 044            if (ZstandardEnabled) methods[count++] = Zstd;
 045            _acceptEncodingHeaderValue = string.Join(", ", methods.Slice(0, count));
 046        }
 47
 048        internal bool GZipEnabled => (_decompressionMethods & DecompressionMethods.GZip) != 0;
 049        internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0;
 050        internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0;
 051        internal bool ZstandardEnabled => (_decompressionMethods & DecompressionMethods.Zstandard) != 0;
 52
 53        private static bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader,
 054        {
 055            foreach (StringWithQualityHeaderValue existingEncoding in acceptEncodingHeader)
 056            {
 057                if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase))
 058                {
 059                    return true;
 60                }
 061            }
 62
 063            return false;
 064        }
 65
 66        internal override async ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, Cancell
 067        {
 068            if (!request.Headers.Contains(KnownHeaders.AcceptEncoding.Descriptor))
 069            {
 70                // Very common case: no Accept-Encoding header yet, so just add one with all supported encodings.
 071                request.Headers.TryAddWithoutValidation(KnownHeaders.AcceptEncoding.Descriptor, _acceptEncodingHeaderVal
 072            }
 73            else
 074            {
 075                HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncoding = request.Headers.AcceptEncoding;
 76
 077                if (GZipEnabled && !EncodingExists(acceptEncoding, Gzip))
 078                {
 079                    acceptEncoding.Add(s_gzipHeaderValue);
 080                }
 81
 082                if (DeflateEnabled && !EncodingExists(acceptEncoding, Deflate))
 083                {
 084                    acceptEncoding.Add(s_deflateHeaderValue);
 085                }
 86
 087                if (BrotliEnabled && !EncodingExists(acceptEncoding, Brotli))
 088                {
 089                    acceptEncoding.Add(s_brotliHeaderValue);
 090                }
 91
 092                if (ZstandardEnabled && !EncodingExists(acceptEncoding, Zstd))
 093                {
 094                    acceptEncoding.Add(s_zstdHeaderValue);
 095                }
 096            }
 97
 098            HttpResponseMessage response = await _innerHandler.SendAsync(request, async, cancellationToken).ConfigureAwa
 99
 0100            Debug.Assert(response.Content != null);
 0101            if (response.Content.Headers.TryGetValues(KnownHeaders.ContentEncoding.Descriptor, out IEnumerable<string>? 
 0102            {
 0103                Debug.Assert(contentEncodings is string[] { Length: > 0 });
 104
 0105                string[] encodings = (string[])contentEncodings;
 0106                string? last = encodings[^1];
 107
 0108                if (GZipEnabled && string.Equals(last, Gzip, StringComparison.OrdinalIgnoreCase))
 0109                {
 0110                    response.Content = new GZipDecompressedContent(response.Content, encodings);
 0111                }
 0112                else if (DeflateEnabled && string.Equals(last, Deflate, StringComparison.OrdinalIgnoreCase))
 0113                {
 0114                    response.Content = new DeflateDecompressedContent(response.Content, encodings);
 0115                }
 0116                else if (BrotliEnabled && string.Equals(last, Brotli, StringComparison.OrdinalIgnoreCase))
 0117                {
 0118                    response.Content = new BrotliDecompressedContent(response.Content, encodings);
 0119                }
 0120                else if (ZstandardEnabled && string.Equals(last, Zstd, StringComparison.OrdinalIgnoreCase))
 0121                {
 0122                    response.Content = new ZstandardDecompressedContent(response.Content, encodings);
 0123                }
 0124            }
 125
 0126            return response;
 0127        }
 128
 129        protected override void Dispose(bool disposing)
 0130        {
 0131            if (disposing)
 0132            {
 0133                _innerHandler.Dispose();
 0134            }
 135
 0136            base.Dispose(disposing);
 0137        }
 138
 139        private abstract class DecompressedContent : HttpContent
 140        {
 141            private readonly HttpContent _originalContent;
 142            private bool _contentConsumed;
 143
 0144            public DecompressedContent(HttpContent originalContent, string[] contentEncodings)
 0145            {
 0146                _originalContent = originalContent;
 0147                _contentConsumed = false;
 148
 149                // Copy original response headers, but with the following changes:
 150                //   Content-Length is removed, since it no longer applies to the decompressed content
 151                //   The last Content-Encoding is removed, since we are processing that here.
 0152                SetHeaders(originalContent.Headers);
 0153                Headers.ContentLength = null;
 0154                Headers.Remove(KnownHeaders.ContentEncoding.Descriptor);
 155
 0156                if (contentEncodings.Length > 1)
 0157                {
 0158                    Headers.TryAddWithoutValidation(KnownHeaders.ContentEncoding.Descriptor, contentEncodings[..^1]);
 0159                }
 0160            }
 161
 162            protected abstract Stream GetDecompressedStream(Stream originalStream);
 163
 164            protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancel
 0165            {
 0166                using Stream decompressedStream = CreateContentReadStream(cancellationToken);
 0167                decompressedStream.CopyTo(stream);
 0168            }
 169
 170            protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) =>
 0171                SerializeToStreamAsync(stream, context, CancellationToken.None);
 172
 173            protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationT
 0174            {
 0175                using (Stream decompressedStream = TryCreateContentReadStream() ?? await CreateContentReadStreamAsync(ca
 0176                {
 0177                    await decompressedStream.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
 0178                }
 0179            }
 180
 181            protected override Stream CreateContentReadStream(CancellationToken cancellationToken)
 0182            {
 0183                ValueTask<Stream> task = CreateContentReadStreamAsyncCore(async: false, cancellationToken);
 0184                Debug.Assert(task.IsCompleted);
 0185                return task.GetAwaiter().GetResult();
 0186            }
 187
 188            protected override Task<Stream> CreateContentReadStreamAsync(CancellationToken cancellationToken) =>
 0189                CreateContentReadStreamAsyncCore(async: true, cancellationToken).AsTask();
 190
 191            private async ValueTask<Stream> CreateContentReadStreamAsyncCore(bool async, CancellationToken cancellationT
 0192            {
 0193                if (_contentConsumed)
 0194                {
 0195                    throw new InvalidOperationException(SR.net_http_content_stream_already_read);
 196                }
 197
 0198                _contentConsumed = true;
 199
 200                Stream originalStream;
 0201                if (async)
 0202                {
 0203                    originalStream = _originalContent.TryReadAsStream() ?? await _originalContent.ReadAsStreamAsync(canc
 0204                }
 205                else
 0206                {
 0207                    originalStream = _originalContent.ReadAsStream(cancellationToken);
 0208                }
 0209                return GetDecompressedStream(originalStream);
 0210            }
 211
 212            internal override Stream? TryCreateContentReadStream()
 0213            {
 0214                Stream? originalStream = _originalContent.TryReadAsStream();
 0215                return originalStream is null ? null : GetDecompressedStream(originalStream);
 0216            }
 217
 218            protected internal override bool TryComputeLength(out long length)
 0219            {
 0220                length = 0;
 0221                return false;
 0222            }
 223
 0224            internal override bool AllowDuplex => false;
 225
 226            protected override void Dispose(bool disposing)
 0227            {
 0228                if (disposing)
 0229                {
 0230                    _originalContent.Dispose();
 0231                }
 0232                base.Dispose(disposing);
 0233            }
 234        }
 235
 0236        private sealed class GZipDecompressedContent(HttpContent originalContent, string[] contentEncodings) : Decompres
 237        {
 238            protected override Stream GetDecompressedStream(Stream originalStream) =>
 0239                new GZipStream(originalStream, CompressionMode.Decompress);
 240        }
 241
 0242        private sealed class DeflateDecompressedContent(HttpContent originalContent, string[] contentEncodings) : Decomp
 243        {
 244            protected override Stream GetDecompressedStream(Stream originalStream) =>
 0245                new ZLibOrDeflateStream(originalStream);
 246
 247            /// <summary>Stream that wraps either <see cref="ZLibStream"/> or <see cref="DeflateStream"/> for decompress
 248            private sealed class ZLibOrDeflateStream : HttpBaseStream
 249            {
 250                // As described in RFC 2616, the deflate content-coding is the "zlib" format (RFC 1950) in combination w
 251                // the "deflate" compression algorithm (RFC 1951). Thus, the right stream to use here is ZLibStream.  Ho
 252                // some servers incorrectly interpret "deflate" to mean the raw, unwrapped deflate protocol.  To account
 253                // that, this switches between using ZLibStream (correct) and DeflateStream (incorrect) in order to maxi
 254                // compatibility with servers.
 255
 256                private readonly PeekFirstByteReadStream _stream;
 257                private Stream? _decompressionStream;
 258
 0259                public ZLibOrDeflateStream(Stream stream) => _stream = new PeekFirstByteReadStream(stream);
 260
 261                protected override void Dispose(bool disposing)
 0262                {
 0263                    if (disposing)
 0264                    {
 0265                        _decompressionStream?.Dispose();
 0266                        _stream.Dispose();
 0267                    }
 0268                    base.Dispose(disposing);
 0269                }
 270
 0271                public override bool CanRead => true;
 0272                public override bool CanWrite => false;
 0273                public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken) =
 274
 275                // On the first read request, peek at the first nibble of the response. If it's an 8, use ZLibStream, ot
 276                // use DeflateStream. This heuristic works because we're deciding only between raw deflate and zlib wrap
 277                // deflate, in which case the first nibble will always be 8 for zlib and never be 8 for deflate.
 278                // https://stackoverflow.com/a/37528114 provides an explanation for why.
 279
 280                public override int Read(Span<byte> buffer)
 0281                {
 0282                    if (_decompressionStream is null)
 0283                    {
 0284                        int firstByte = _stream.PeekFirstByte();
 0285                        _decompressionStream = CreateDecompressionStream(firstByte, _stream);
 0286                    }
 287
 0288                    return _decompressionStream.Read(buffer);
 0289                }
 290
 291                public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
 0292                {
 0293                    if (_decompressionStream is null)
 0294                    {
 0295                        return CreateAndReadAsync(this, buffer, cancellationToken);
 296
 297                        static async ValueTask<int> CreateAndReadAsync(ZLibOrDeflateStream thisRef, Memory<byte> buffer,
 0298                        {
 0299                            int firstByte = await thisRef._stream.PeekFirstByteAsync(cancellationToken).ConfigureAwait(f
 0300                            thisRef._decompressionStream = CreateDecompressionStream(firstByte, thisRef._stream);
 0301                            return await thisRef._decompressionStream.ReadAsync(buffer, cancellationToken).ConfigureAwai
 0302                        }
 303                    }
 304
 0305                    return _decompressionStream.ReadAsync(buffer, cancellationToken);
 0306                }
 307
 308                public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken
 0309                {
 0310                    ValidateCopyToArguments(destination, bufferSize);
 0311                    return Core(destination, bufferSize, cancellationToken);
 312                    async Task Core(Stream destination, int bufferSize, CancellationToken cancellationToken)
 0313                    {
 0314                        if (_decompressionStream is null)
 0315                        {
 0316                            int firstByte = await _stream.PeekFirstByteAsync(cancellationToken).ConfigureAwait(false);
 0317                            _decompressionStream = CreateDecompressionStream(firstByte, _stream);
 0318                        }
 319
 0320                        await _decompressionStream.CopyToAsync(destination, bufferSize, cancellationToken).ConfigureAwai
 0321                    }
 0322                }
 323
 324                private static Stream CreateDecompressionStream(int firstByte, Stream stream) =>
 0325                    (firstByte & 0xF) == 8 ?
 0326                        new ZLibStream(stream, CompressionMode.Decompress) :
 0327                        new DeflateStream(stream, CompressionMode.Decompress);
 328
 329                private sealed class PeekFirstByteReadStream : HttpBaseStream
 330                {
 331                    private readonly Stream _stream;
 332                    private byte _firstByte;
 333                    private FirstByteStatus _firstByteStatus;
 334
 0335                    public PeekFirstByteReadStream(Stream stream) => _stream = stream;
 336
 337                    protected override void Dispose(bool disposing)
 0338                    {
 0339                        if (disposing)
 0340                        {
 0341                            _stream.Dispose();
 0342                        }
 0343                        base.Dispose(disposing);
 0344                    }
 345
 0346                    public override bool CanRead => true;
 0347                    public override bool CanWrite => false;
 0348                    public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToke
 349
 350                    public int PeekFirstByte()
 0351                    {
 0352                        Debug.Assert(_firstByteStatus == FirstByteStatus.None);
 353
 0354                        int value = _stream.ReadByte();
 0355                        if (value == -1)
 0356                        {
 0357                            _firstByteStatus = FirstByteStatus.Consumed;
 0358                            return -1;
 359                        }
 360
 0361                        _firstByte = (byte)value;
 0362                        _firstByteStatus = FirstByteStatus.Available;
 0363                        return value;
 0364                    }
 365
 366                    public async ValueTask<int> PeekFirstByteAsync(CancellationToken cancellationToken)
 0367                    {
 0368                        Debug.Assert(_firstByteStatus == FirstByteStatus.None);
 369
 0370                        var buffer = new byte[1];
 371
 0372                        int bytesRead = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 0373                        if (bytesRead == 0)
 0374                        {
 0375                            _firstByteStatus = FirstByteStatus.Consumed;
 0376                            return -1;
 377                        }
 378
 0379                        _firstByte = buffer[0];
 0380                        _firstByteStatus = FirstByteStatus.Available;
 0381                        return buffer[0];
 0382                    }
 383
 384                    public override int Read(Span<byte> buffer)
 0385                    {
 0386                        if (_firstByteStatus == FirstByteStatus.Available)
 0387                        {
 0388                            if (buffer.Length != 0)
 0389                            {
 0390                                buffer[0] = _firstByte;
 0391                                _firstByteStatus = FirstByteStatus.Consumed;
 0392                                return 1;
 393                            }
 394
 0395                            return 0;
 396                        }
 397
 0398                        Debug.Assert(_firstByteStatus == FirstByteStatus.Consumed);
 0399                        return _stream.Read(buffer);
 0400                    }
 401
 402                    public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
 0403                    {
 0404                        if (_firstByteStatus == FirstByteStatus.Available)
 0405                        {
 0406                            if (buffer.Length != 0)
 0407                            {
 0408                                buffer.Span[0] = _firstByte;
 0409                                _firstByteStatus = FirstByteStatus.Consumed;
 0410                                return new ValueTask<int>(1);
 411                            }
 412
 0413                            return new ValueTask<int>(0);
 414                        }
 415
 0416                        Debug.Assert(_firstByteStatus == FirstByteStatus.Consumed);
 0417                        return _stream.ReadAsync(buffer, cancellationToken);
 0418                    }
 419
 420                    public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancell
 0421                    {
 0422                        Debug.Assert(_firstByteStatus != FirstByteStatus.None);
 423
 0424                        ValidateCopyToArguments(destination, bufferSize);
 0425                        if (_firstByteStatus == FirstByteStatus.Available)
 0426                        {
 0427                            await destination.WriteAsync(new byte[] { _firstByte }, cancellationToken).ConfigureAwait(fa
 0428                            _firstByteStatus = FirstByteStatus.Consumed;
 0429                        }
 430
 0431                        await _stream.CopyToAsync(destination, bufferSize, cancellationToken).ConfigureAwait(false);
 0432                    }
 433
 434                    private enum FirstByteStatus : byte
 435                    {
 436                        None = 0,
 437                        Available = 1,
 438                        Consumed = 2
 439                    }
 440                }
 441            }
 442        }
 443
 0444        private sealed class BrotliDecompressedContent(HttpContent originalContent, string[] contentEncodings) : Decompr
 445        {
 446            protected override Stream GetDecompressedStream(Stream originalStream) =>
 0447                new BrotliStream(originalStream, CompressionMode.Decompress);
 448        }
 449
 0450        private sealed class ZstandardDecompressedContent(HttpContent originalContent, string[] contentEncodings) : Deco
 451        {
 452            // RFC 9659 §3.2 mandates HTTP implementations cap decompression window size at 8 MB (WindowLog=23).
 0453            private static readonly ZstandardDecompressionOptions s_decompressionOptions = new ZstandardDecompressionOpt
 454
 455            protected override Stream GetDecompressedStream(Stream originalStream) =>
 0456                new ZstandardStream(originalStream, s_decompressionOptions);
 457        }
 458    }
 459}
 460

Methods/Properties

.cctor()
.ctor(System.Net.DecompressionMethods,System.Net.Http.HttpMessageHandlerStage)
GZipEnabled()
DeflateEnabled()
BrotliEnabled()
ZstandardEnabled()
EncodingExists(System.Net.Http.Headers.HttpHeaderValueCollection`1<System.Net.Http.Headers.StringWithQualityHeaderValue>,System.String)
SendAsync(System.Net.Http.HttpRequestMessage,System.Boolean,System.Threading.CancellationToken)
Dispose(System.Boolean)
.ctor(System.Net.Http.HttpContent,System.String[])
SerializeToStream(System.IO.Stream,System.Net.TransportContext,System.Threading.CancellationToken)
SerializeToStreamAsync(System.IO.Stream,System.Net.TransportContext)
SerializeToStreamAsync(System.IO.Stream,System.Net.TransportContext,System.Threading.CancellationToken)
CreateContentReadStream(System.Threading.CancellationToken)
CreateContentReadStreamAsync(System.Threading.CancellationToken)
CreateContentReadStreamAsyncCore(System.Boolean,System.Threading.CancellationToken)
TryCreateContentReadStream()
TryComputeLength(System.Int64&)
AllowDuplex()
Dispose(System.Boolean)
.ctor(System.Net.Http.HttpContent,System.String[])
GetDecompressedStream(System.IO.Stream)
.ctor(System.Net.Http.HttpContent,System.String[])
GetDecompressedStream(System.IO.Stream)
.ctor(System.IO.Stream)
Dispose(System.Boolean)
CanRead()
CanWrite()
WriteAsync(System.ReadOnlyMemory`1<System.Byte>,System.Threading.CancellationToken)
Read(System.Span`1<System.Byte>)
ReadAsync(System.Memory`1<System.Byte>,System.Threading.CancellationToken)
CreateAndReadAsync(System.Net.Http.DecompressionHandler/DeflateDecompressedContent/ZLibOrDeflateStream,System.Memory`1<System.Byte>,System.Threading.CancellationToken)
CopyToAsync(System.IO.Stream,System.Int32,System.Threading.CancellationToken)
Core(System.IO.Stream,System.Int32,System.Threading.CancellationToken)
CreateDecompressionStream(System.Int32,System.IO.Stream)
.ctor(System.IO.Stream)
Dispose(System.Boolean)
CanRead()
CanWrite()
WriteAsync(System.ReadOnlyMemory`1<System.Byte>,System.Threading.CancellationToken)
PeekFirstByte()
PeekFirstByteAsync(System.Threading.CancellationToken)
Read(System.Span`1<System.Byte>)
ReadAsync(System.Memory`1<System.Byte>,System.Threading.CancellationToken)
CopyToAsync(System.IO.Stream,System.Int32,System.Threading.CancellationToken)
.ctor(System.Net.Http.HttpContent,System.String[])
GetDecompressedStream(System.IO.Stream)
.ctor(System.Net.Http.HttpContent,System.String[])
.cctor()
GetDecompressedStream(System.IO.Stream)