< Summary

Information
Class: System.Net.Http.DecompressionHandler
Assembly: System.Net.Http
File(s): D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\DecompressionHandler.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 253
Coverable lines: 253
Total lines: 434
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 84
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

D:\runner\runtime\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";
 022        private static readonly StringWithQualityHeaderValue s_gzipHeaderValue = new(Gzip);
 023        private static readonly StringWithQualityHeaderValue s_deflateHeaderValue = new(Deflate);
 024        private static readonly StringWithQualityHeaderValue s_brotliHeaderValue = new(Brotli);
 25
 26        /// <summary>Header value for all enabled decompression methods, e.g. "gzip, deflate".</summary>
 27        private readonly string _acceptEncodingHeaderValue;
 28
 029        public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessageHandlerStage innerHandler)
 030        {
 031            Debug.Assert(decompressionMethods != DecompressionMethods.None);
 032            Debug.Assert(innerHandler != null);
 33
 034            _decompressionMethods = decompressionMethods;
 035            _innerHandler = innerHandler;
 36
 037            List<string?> methods = [GZipEnabled ? Gzip : null, DeflateEnabled ? Deflate : null, BrotliEnabled ? Brotli 
 038            methods.RemoveAll(item => item is null);
 039            _acceptEncodingHeaderValue = string.Join(", ", methods);
 040        }
 41
 042        internal bool GZipEnabled => (_decompressionMethods & DecompressionMethods.GZip) != 0;
 043        internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0;
 044        internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0;
 45
 46        private static bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader,
 047        {
 048            foreach (StringWithQualityHeaderValue existingEncoding in acceptEncodingHeader)
 049            {
 050                if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase))
 051                {
 052                    return true;
 53                }
 054            }
 55
 056            return false;
 057        }
 58
 59        internal override async ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, Cancell
 060        {
 061            if (!request.Headers.Contains(KnownHeaders.AcceptEncoding.Descriptor))
 062            {
 63                // Very common case: no Accept-Encoding header yet, so just add one with all supported encodings.
 064                request.Headers.TryAddWithoutValidation(KnownHeaders.AcceptEncoding.Descriptor, _acceptEncodingHeaderVal
 065            }
 66            else
 067            {
 068                HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncoding = request.Headers.AcceptEncoding;
 69
 070                if (GZipEnabled && !EncodingExists(acceptEncoding, Gzip))
 071                {
 072                    acceptEncoding.Add(s_gzipHeaderValue);
 073                }
 74
 075                if (DeflateEnabled && !EncodingExists(acceptEncoding, Deflate))
 076                {
 077                    acceptEncoding.Add(s_deflateHeaderValue);
 078                }
 79
 080                if (BrotliEnabled && !EncodingExists(acceptEncoding, Brotli))
 081                {
 082                    acceptEncoding.Add(s_brotliHeaderValue);
 083                }
 084            }
 85
 086            HttpResponseMessage response = await _innerHandler.SendAsync(request, async, cancellationToken).ConfigureAwa
 87
 088            Debug.Assert(response.Content != null);
 089            if (response.Content.Headers.TryGetValues(KnownHeaders.ContentEncoding.Descriptor, out IEnumerable<string>? 
 090            {
 091                Debug.Assert(contentEncodings is string[] { Length: > 0 });
 92
 093                string[] encodings = (string[])contentEncodings;
 094                string? last = encodings[^1];
 95
 096                if (GZipEnabled && string.Equals(last, Gzip, StringComparison.OrdinalIgnoreCase))
 097                {
 098                    response.Content = new GZipDecompressedContent(response.Content, encodings);
 099                }
 0100                else if (DeflateEnabled && string.Equals(last, Deflate, StringComparison.OrdinalIgnoreCase))
 0101                {
 0102                    response.Content = new DeflateDecompressedContent(response.Content, encodings);
 0103                }
 0104                else if (BrotliEnabled && string.Equals(last, Brotli, StringComparison.OrdinalIgnoreCase))
 0105                {
 0106                    response.Content = new BrotliDecompressedContent(response.Content, encodings);
 0107                }
 0108            }
 109
 0110            return response;
 0111        }
 112
 113        protected override void Dispose(bool disposing)
 0114        {
 0115            if (disposing)
 0116            {
 0117                _innerHandler.Dispose();
 0118            }
 119
 0120            base.Dispose(disposing);
 0121        }
 122
 123        private abstract class DecompressedContent : HttpContent
 124        {
 125            private readonly HttpContent _originalContent;
 126            private bool _contentConsumed;
 127
 0128            public DecompressedContent(HttpContent originalContent, string[] contentEncodings)
 0129            {
 0130                _originalContent = originalContent;
 0131                _contentConsumed = false;
 132
 133                // Copy original response headers, but with the following changes:
 134                //   Content-Length is removed, since it no longer applies to the decompressed content
 135                //   The last Content-Encoding is removed, since we are processing that here.
 0136                SetHeaders(originalContent.Headers);
 0137                Headers.ContentLength = null;
 0138                Headers.Remove(KnownHeaders.ContentEncoding.Descriptor);
 139
 0140                if (contentEncodings.Length > 1)
 0141                {
 0142                    Headers.TryAddWithoutValidation(KnownHeaders.ContentEncoding.Descriptor, contentEncodings[..^1]);
 0143                }
 0144            }
 145
 146            protected abstract Stream GetDecompressedStream(Stream originalStream);
 147
 148            protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancel
 0149            {
 0150                using Stream decompressedStream = CreateContentReadStream(cancellationToken);
 0151                decompressedStream.CopyTo(stream);
 0152            }
 153
 154            protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) =>
 0155                SerializeToStreamAsync(stream, context, CancellationToken.None);
 156
 157            protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationT
 0158            {
 0159                using (Stream decompressedStream = TryCreateContentReadStream() ?? await CreateContentReadStreamAsync(ca
 0160                {
 0161                    await decompressedStream.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
 0162                }
 0163            }
 164
 165            protected override Stream CreateContentReadStream(CancellationToken cancellationToken)
 0166            {
 0167                ValueTask<Stream> task = CreateContentReadStreamAsyncCore(async: false, cancellationToken);
 0168                Debug.Assert(task.IsCompleted);
 0169                return task.GetAwaiter().GetResult();
 0170            }
 171
 172            protected override Task<Stream> CreateContentReadStreamAsync(CancellationToken cancellationToken) =>
 0173                CreateContentReadStreamAsyncCore(async: true, cancellationToken).AsTask();
 174
 175            private async ValueTask<Stream> CreateContentReadStreamAsyncCore(bool async, CancellationToken cancellationT
 0176            {
 0177                if (_contentConsumed)
 0178                {
 0179                    throw new InvalidOperationException(SR.net_http_content_stream_already_read);
 180                }
 181
 0182                _contentConsumed = true;
 183
 184                Stream originalStream;
 0185                if (async)
 0186                {
 0187                    originalStream = _originalContent.TryReadAsStream() ?? await _originalContent.ReadAsStreamAsync(canc
 0188                }
 189                else
 0190                {
 0191                    originalStream = _originalContent.ReadAsStream(cancellationToken);
 0192                }
 0193                return GetDecompressedStream(originalStream);
 0194            }
 195
 196            internal override Stream? TryCreateContentReadStream()
 0197            {
 0198                Stream? originalStream = _originalContent.TryReadAsStream();
 0199                return originalStream is null ? null : GetDecompressedStream(originalStream);
 0200            }
 201
 202            protected internal override bool TryComputeLength(out long length)
 0203            {
 0204                length = 0;
 0205                return false;
 0206            }
 207
 0208            internal override bool AllowDuplex => false;
 209
 210            protected override void Dispose(bool disposing)
 0211            {
 0212                if (disposing)
 0213                {
 0214                    _originalContent.Dispose();
 0215                }
 0216                base.Dispose(disposing);
 0217            }
 218        }
 219
 0220        private sealed class GZipDecompressedContent(HttpContent originalContent, string[] contentEncodings) : Decompres
 221        {
 222            protected override Stream GetDecompressedStream(Stream originalStream) =>
 0223                new GZipStream(originalStream, CompressionMode.Decompress);
 224        }
 225
 0226        private sealed class DeflateDecompressedContent(HttpContent originalContent, string[] contentEncodings) : Decomp
 227        {
 228            protected override Stream GetDecompressedStream(Stream originalStream) =>
 0229                new ZLibOrDeflateStream(originalStream);
 230
 231            /// <summary>Stream that wraps either <see cref="ZLibStream"/> or <see cref="DeflateStream"/> for decompress
 232            private sealed class ZLibOrDeflateStream : HttpBaseStream
 233            {
 234                // As described in RFC 2616, the deflate content-coding is the "zlib" format (RFC 1950) in combination w
 235                // the "deflate" compression algorithm (RFC 1951). Thus, the right stream to use here is ZLibStream.  Ho
 236                // some servers incorrectly interpret "deflate" to mean the raw, unwrapped deflate protocol.  To account
 237                // that, this switches between using ZLibStream (correct) and DeflateStream (incorrect) in order to maxi
 238                // compatibility with servers.
 239
 240                private readonly PeekFirstByteReadStream _stream;
 241                private Stream? _decompressionStream;
 242
 0243                public ZLibOrDeflateStream(Stream stream) => _stream = new PeekFirstByteReadStream(stream);
 244
 245                protected override void Dispose(bool disposing)
 0246                {
 0247                    if (disposing)
 0248                    {
 0249                        _decompressionStream?.Dispose();
 0250                        _stream.Dispose();
 0251                    }
 0252                    base.Dispose(disposing);
 0253                }
 254
 0255                public override bool CanRead => true;
 0256                public override bool CanWrite => false;
 0257                public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken) =
 258
 259                // On the first read request, peek at the first nibble of the response. If it's an 8, use ZLibStream, ot
 260                // use DeflateStream. This heuristic works because we're deciding only between raw deflate and zlib wrap
 261                // deflate, in which case the first nibble will always be 8 for zlib and never be 8 for deflate.
 262                // https://stackoverflow.com/a/37528114 provides an explanation for why.
 263
 264                public override int Read(Span<byte> buffer)
 0265                {
 0266                    if (_decompressionStream is null)
 0267                    {
 0268                        int firstByte = _stream.PeekFirstByte();
 0269                        _decompressionStream = CreateDecompressionStream(firstByte, _stream);
 0270                    }
 271
 0272                    return _decompressionStream.Read(buffer);
 0273                }
 274
 275                public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
 0276                {
 0277                    if (_decompressionStream is null)
 0278                    {
 0279                        return CreateAndReadAsync(this, buffer, cancellationToken);
 280
 281                        static async ValueTask<int> CreateAndReadAsync(ZLibOrDeflateStream thisRef, Memory<byte> buffer,
 0282                        {
 0283                            int firstByte = await thisRef._stream.PeekFirstByteAsync(cancellationToken).ConfigureAwait(f
 0284                            thisRef._decompressionStream = CreateDecompressionStream(firstByte, thisRef._stream);
 0285                            return await thisRef._decompressionStream.ReadAsync(buffer, cancellationToken).ConfigureAwai
 0286                        }
 287                    }
 288
 0289                    return _decompressionStream.ReadAsync(buffer, cancellationToken);
 0290                }
 291
 292                public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken
 0293                {
 0294                    ValidateCopyToArguments(destination, bufferSize);
 0295                    return Core(destination, bufferSize, cancellationToken);
 296                    async Task Core(Stream destination, int bufferSize, CancellationToken cancellationToken)
 0297                    {
 0298                        if (_decompressionStream is null)
 0299                        {
 0300                            int firstByte = await _stream.PeekFirstByteAsync(cancellationToken).ConfigureAwait(false);
 0301                            _decompressionStream = CreateDecompressionStream(firstByte, _stream);
 0302                        }
 303
 0304                        await _decompressionStream.CopyToAsync(destination, bufferSize, cancellationToken).ConfigureAwai
 0305                    }
 0306                }
 307
 308                private static Stream CreateDecompressionStream(int firstByte, Stream stream) =>
 0309                    (firstByte & 0xF) == 8 ?
 0310                        new ZLibStream(stream, CompressionMode.Decompress) :
 0311                        new DeflateStream(stream, CompressionMode.Decompress);
 312
 313                private sealed class PeekFirstByteReadStream : HttpBaseStream
 314                {
 315                    private readonly Stream _stream;
 316                    private byte _firstByte;
 317                    private FirstByteStatus _firstByteStatus;
 318
 0319                    public PeekFirstByteReadStream(Stream stream) => _stream = stream;
 320
 321                    protected override void Dispose(bool disposing)
 0322                    {
 0323                        if (disposing)
 0324                        {
 0325                            _stream.Dispose();
 0326                        }
 0327                        base.Dispose(disposing);
 0328                    }
 329
 0330                    public override bool CanRead => true;
 0331                    public override bool CanWrite => false;
 0332                    public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToke
 333
 334                    public int PeekFirstByte()
 0335                    {
 0336                        Debug.Assert(_firstByteStatus == FirstByteStatus.None);
 337
 0338                        int value = _stream.ReadByte();
 0339                        if (value == -1)
 0340                        {
 0341                            _firstByteStatus = FirstByteStatus.Consumed;
 0342                            return -1;
 343                        }
 344
 0345                        _firstByte = (byte)value;
 0346                        _firstByteStatus = FirstByteStatus.Available;
 0347                        return value;
 0348                    }
 349
 350                    public async ValueTask<int> PeekFirstByteAsync(CancellationToken cancellationToken)
 0351                    {
 0352                        Debug.Assert(_firstByteStatus == FirstByteStatus.None);
 353
 0354                        var buffer = new byte[1];
 355
 0356                        int bytesRead = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 0357                        if (bytesRead == 0)
 0358                        {
 0359                            _firstByteStatus = FirstByteStatus.Consumed;
 0360                            return -1;
 361                        }
 362
 0363                        _firstByte = buffer[0];
 0364                        _firstByteStatus = FirstByteStatus.Available;
 0365                        return buffer[0];
 0366                    }
 367
 368                    public override int Read(Span<byte> buffer)
 0369                    {
 0370                        if (_firstByteStatus == FirstByteStatus.Available)
 0371                        {
 0372                            if (buffer.Length != 0)
 0373                            {
 0374                                buffer[0] = _firstByte;
 0375                                _firstByteStatus = FirstByteStatus.Consumed;
 0376                                return 1;
 377                            }
 378
 0379                            return 0;
 380                        }
 381
 0382                        Debug.Assert(_firstByteStatus == FirstByteStatus.Consumed);
 0383                        return _stream.Read(buffer);
 0384                    }
 385
 386                    public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
 0387                    {
 0388                        if (_firstByteStatus == FirstByteStatus.Available)
 0389                        {
 0390                            if (buffer.Length != 0)
 0391                            {
 0392                                buffer.Span[0] = _firstByte;
 0393                                _firstByteStatus = FirstByteStatus.Consumed;
 0394                                return new ValueTask<int>(1);
 395                            }
 396
 0397                            return new ValueTask<int>(0);
 398                        }
 399
 0400                        Debug.Assert(_firstByteStatus == FirstByteStatus.Consumed);
 0401                        return _stream.ReadAsync(buffer, cancellationToken);
 0402                    }
 403
 404                    public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancell
 0405                    {
 0406                        Debug.Assert(_firstByteStatus != FirstByteStatus.None);
 407
 0408                        ValidateCopyToArguments(destination, bufferSize);
 0409                        if (_firstByteStatus == FirstByteStatus.Available)
 0410                        {
 0411                            await destination.WriteAsync(new byte[] { _firstByte }, cancellationToken).ConfigureAwait(fa
 0412                            _firstByteStatus = FirstByteStatus.Consumed;
 0413                        }
 414
 0415                        await _stream.CopyToAsync(destination, bufferSize, cancellationToken).ConfigureAwait(false);
 0416                    }
 417
 418                    private enum FirstByteStatus : byte
 419                    {
 420                        None = 0,
 421                        Available = 1,
 422                        Consumed = 2
 423                    }
 424                }
 425            }
 426        }
 427
 0428        private sealed class BrotliDecompressedContent(HttpContent originalContent, string[] contentEncodings) : Decompr
 429        {
 430            protected override Stream GetDecompressedStream(Stream originalStream) =>
 0431                new BrotliStream(originalStream, CompressionMode.Decompress);
 432        }
 433    }
 434}

Methods/Properties

.cctor()
.ctor(System.Net.DecompressionMethods,System.Net.Http.HttpMessageHandlerStage)
GZipEnabled()
DeflateEnabled()
BrotliEnabled()
EncodingExists(System.Net.Http.Headers.HttpHeaderValueCollection`1<System.Net.Http.Headers.StringWithQualityHeaderValue>,System.String)
SendAsync()
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()
CreateContentReadStream(System.Threading.CancellationToken)
CreateContentReadStreamAsync(System.Threading.CancellationToken)
CreateContentReadStreamAsyncCore()
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()
CopyToAsync(System.IO.Stream,System.Int32,System.Threading.CancellationToken)
Core()
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()
Read(System.Span`1<System.Byte>)
ReadAsync(System.Memory`1<System.Byte>,System.Threading.CancellationToken)
CopyToAsync()
.ctor(System.Net.Http.HttpContent,System.String[])
GetDecompressedStream(System.IO.Stream)