File: System\Net\HttpWebResponse.cs
Web Access
Project: src\src\libraries\System.Net.Requests\src\System.Net.Requests.csproj (System.Net.Requests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net
{
    /// <devdoc>
    ///    <para>
    ///    An HTTP-specific implementation of the
    ///    <see cref='System.Net.WebResponse'/> class.
    /// </para>
    /// </devdoc>
    public class HttpWebResponse : WebResponse, ISerializable
    {
        private HttpResponseMessage _httpResponseMessage = null!;
        private readonly Uri _requestUri;
        private CookieCollection _cookies;
        private WebHeaderCollection? _webHeaderCollection;
        private string? _characterSet;
        private readonly bool _isVersionHttp11 = true;
 
        [Obsolete("This API supports the .NET infrastructure and is not intended to be used directly from your code.", true)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public HttpWebResponse()
        {
            _requestUri = null!;
            _cookies = null!;
        }
 
        [Obsolete("Serialization has been deprecated for HttpWebResponse.")]
        [EditorBrowsable(EditorBrowsableState.Never)]
        protected HttpWebResponse(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext)
        {
            throw new PlatformNotSupportedException();
        }
 
        [Obsolete("Serialization has been deprecated for HttpWebResponse.")]
        void ISerializable.GetObjectData(SerializationInfo serializationInfo, StreamingContext streamingContext)
        {
            throw new PlatformNotSupportedException();
        }
 
        [Obsolete("Serialization has been deprecated for HttpWebResponse.")]
        protected override void GetObjectData(SerializationInfo serializationInfo, StreamingContext streamingContext)
        {
            throw new PlatformNotSupportedException();
        }
 
        internal HttpWebResponse(HttpResponseMessage _message, Uri requestUri, CookieContainer? cookieContainer)
        {
            _httpResponseMessage = _message;
            _requestUri = requestUri;
 
            // Match Desktop behavior. If the request didn't set a CookieContainer, we don't populate the response's CookieCollection.
            if (cookieContainer != null)
            {
                _cookies = cookieContainer.GetCookies(requestUri);
            }
            else
            {
                _cookies = new CookieCollection();
            }
        }
        public override bool IsMutuallyAuthenticated
        {
            get
            {
                return base.IsMutuallyAuthenticated;
            }
        }
 
        public override long ContentLength
        {
            get
            {
                CheckDisposed();
                return _httpResponseMessage.Content?.Headers.ContentLength ?? -1;
            }
        }
 
        public override string ContentType
        {
            get
            {
                CheckDisposed();
 
                // We use TryGetValues() instead of the strongly type Headers.ContentType property so that
                // we return a string regardless of it being fully RFC conformant. This matches current
                // .NET Framework behavior.
                if (_httpResponseMessage.Content != null && _httpResponseMessage.Content.Headers.TryGetValues("Content-Type", out IEnumerable<string>? values))
                {
                    // In most cases, there is only one media type value as per RFC. But for completeness, we
                    // return all values in cases of overly malformed strings.
                    return string.Join(',', values);
                }
                else
                {
                    return string.Empty;
                }
            }
        }
 
        public string ContentEncoding
        {
            get
            {
                CheckDisposed();
                if (_httpResponseMessage.Content != null)
                {
                    return GetHeaderValueAsString(_httpResponseMessage.Content.Headers.ContentEncoding);
                }
 
                return string.Empty;
            }
        }
 
        public virtual CookieCollection Cookies
        {
            get
            {
                CheckDisposed();
                return _cookies;
            }
 
            set
            {
                CheckDisposed();
                _cookies = value;
            }
        }
 
        public DateTime LastModified
        {
            get
            {
                CheckDisposed();
                string? lastmodHeaderValue = Headers["Last-Modified"];
                if (string.IsNullOrEmpty(lastmodHeaderValue))
                {
                    return DateTime.Now;
                }
 
                if (HttpDateParser.TryParse(lastmodHeaderValue, out DateTimeOffset dateTimeOffset))
                {
                    return dateTimeOffset.LocalDateTime;
                }
                else
                {
                    throw new ProtocolViolationException(SR.net_baddate);
                }
            }
        }
 
        /// <devdoc>
        ///    <para>
        ///       Gets the name of the server that sent the response.
        ///    </para>
        /// </devdoc>
        public string Server
        {
            get
            {
                CheckDisposed();
                string? server = Headers["Server"];
                return string.IsNullOrEmpty(server) ? string.Empty : server;
            }
        }
 
        // HTTP Version
        /// <devdoc>
        ///    <para>
        ///       Gets
        ///       the version of the HTTP protocol used in the response.
        ///    </para>
        /// </devdoc>
        public Version ProtocolVersion
        {
            get
            {
                CheckDisposed();
                return _isVersionHttp11 ? HttpVersion.Version11 : HttpVersion.Version10;
            }
        }
 
        public override WebHeaderCollection Headers
        {
            get
            {
                CheckDisposed();
                if (_webHeaderCollection == null)
                {
                    _webHeaderCollection = new WebHeaderCollection();
 
                    foreach (var header in _httpResponseMessage.Headers)
                    {
                        _webHeaderCollection[header.Key] = GetHeaderValueAsString(header.Value);
                    }
 
                    if (_httpResponseMessage.Content != null)
                    {
                        foreach (var header in _httpResponseMessage.Content.Headers)
                        {
                            _webHeaderCollection[header.Key] = GetHeaderValueAsString(header.Value);
                        }
                    }
                }
                return _webHeaderCollection;
            }
        }
 
        public virtual string Method
        {
            get
            {
                CheckDisposed();
                return _httpResponseMessage.RequestMessage!.Method.Method;
            }
        }
 
        public override Uri ResponseUri
        {
            get
            {
                CheckDisposed();
 
                // The underlying System.Net.Http API will automatically update
                // the .RequestUri property to be the final URI of the response.
                return _httpResponseMessage.RequestMessage!.RequestUri!;
            }
        }
 
        public virtual HttpStatusCode StatusCode
        {
            get
            {
                CheckDisposed();
                return _httpResponseMessage.StatusCode;
            }
        }
 
        public virtual string StatusDescription
        {
            get
            {
                CheckDisposed();
                return _httpResponseMessage.ReasonPhrase ?? string.Empty;
            }
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public string? CharacterSet
        {
            get
            {
                CheckDisposed();
                string? contentType = Headers["Content-Type"];
 
                if (_characterSet == null && !string.IsNullOrWhiteSpace(contentType))
                {
                    //sets characterset so the branch is never executed again.
                    _characterSet = string.Empty;
 
                    //first string is the media type
                    string srchString = contentType.ToLowerInvariant();
 
                    //media subtypes of text type has a default as specified by rfc 2616
                    if (srchString.AsSpan().Trim().StartsWith("text/", StringComparison.Ordinal))
                    {
                        _characterSet = "ISO-8859-1";
                    }
 
                    //one of the parameters may be the character set
                    //there must be at least a mediatype for this to be valid
                    int i = srchString.IndexOf(';');
                    if (i > 0)
                    {
                        //search the parameters
                        while ((i = srchString.IndexOf("charset", i, StringComparison.Ordinal)) >= 0)
                        {
                            i += 7;
 
                            //make sure the word starts with charset
                            if (srchString[i - 8] == ';' || srchString[i - 8] == ' ')
                            {
                                //skip whitespace
                                while (i < srchString.Length && srchString[i] == ' ')
                                    i++;
 
                                //only process if next character is '='
                                //and there is a character after that
                                if (i < srchString.Length - 1 && srchString[i] == '=')
                                {
                                    i++;
 
                                    //get and trim character set substring
                                    int j = srchString.IndexOf(';', i);
                                    //In the past we used
                                    //Substring(i, j). J is the offset not the length
                                    //the qfe is to fix the second parameter so that this it is the
                                    //length. since j points to the next ; the operation j -i
                                    //gives the length of the charset
                                    if (j > i)
                                        _characterSet = contentType.AsSpan(i, j - i).Trim().ToString();
                                    else
                                        _characterSet = contentType.AsSpan(i).Trim().ToString();
 
                                    //done
                                    break;
                                }
                            }
                        }
                    }
                }
                return _characterSet;
            }
        }
 
        public override bool SupportsHeaders
        {
            get
            {
                return true;
            }
        }
 
        public override Stream GetResponseStream()
        {
            CheckDisposed();
            if (_httpResponseMessage.Content != null)
            {
                Stream contentStream = _httpResponseMessage.Content.ReadAsStream();
                int maxErrorResponseLength = HttpWebRequest.DefaultMaximumErrorResponseLength;
                if (maxErrorResponseLength < 0 || StatusCode < HttpStatusCode.BadRequest)
                {
                    return contentStream;
                }
 
                return new TruncatedReadStream(contentStream, (long)maxErrorResponseLength * 1024);
            }
 
            return Stream.Null;
        }
 
        public string GetResponseHeader(string headerName)
        {
            CheckDisposed();
            string? headerValue = Headers[headerName];
            return headerValue ?? string.Empty;
        }
 
        public override void Close()
        {
            Dispose(true);
        }
 
        protected override void Dispose(bool disposing)
        {
            var httpResponseMessage = _httpResponseMessage;
            if (httpResponseMessage != null)
            {
                httpResponseMessage.Dispose();
                _httpResponseMessage = null!;
            }
        }
 
        private void CheckDisposed()
        {
            ObjectDisposedException.ThrowIf(_httpResponseMessage == null, this);
        }
 
        private static string GetHeaderValueAsString(IEnumerable<string> values) => string.Join(", ", values);
 
        internal sealed class TruncatedReadStream(Stream innerStream, long maxSize) : Stream
        {
            private long _maxRemainingLength = maxSize;
            public override bool CanRead => true;
            public override bool CanSeek => false;
            public override bool CanWrite => false;
 
            public override long Length => throw new NotSupportedException();
            public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
 
            public override void Flush() => throw new NotSupportedException();
 
            public override int Read(byte[] buffer, int offset, int count)
            {
                return Read(new Span<byte>(buffer, offset, count));
            }
 
            public override int Read(Span<byte> buffer)
            {
                int readBytes = innerStream.Read(buffer.Slice(0, (int)Math.Min(buffer.Length, _maxRemainingLength)));
                _maxRemainingLength -= readBytes;
                return readBytes;
            }
 
            public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
            {
                return ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
            }
 
            public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
            {
                int readBytes = await innerStream.ReadAsync(buffer.Slice(0, (int)Math.Min(buffer.Length, _maxRemainingLength)), cancellationToken)
                    .ConfigureAwait(false);
                _maxRemainingLength -= readBytes;
                return readBytes;
            }
 
            public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
            public override void SetLength(long value) => throw new NotSupportedException();
            public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
 
            public override ValueTask DisposeAsync() => innerStream.DisposeAsync();
 
            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {
                    innerStream.Dispose();
                }
            }
        }
    }
}