File: System\Net\Http\Json\HttpClientJsonExtensions.cs
Web Access
Project: src\src\libraries\System.Net.Http.Json\src\System.Net.Http.Json.csproj (System.Net.Http.Json)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net.Http.Json
{
    public static partial class HttpClientJsonExtensions
    {
        [RequiresUnreferencedCode(HttpContentJsonExtensions.SerializationUnreferencedCodeMessage)]
        [RequiresDynamicCode(HttpContentJsonExtensions.SerializationDynamicCodeMessage)]
        private static Task<object?> FromJsonAsyncCore(Func<HttpClient, Uri?, CancellationToken, Task<HttpResponseMessage>> getMethod, HttpClient client, Uri? requestUri, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken = default) =>
            FromJsonAsyncCore(getMethod, client, requestUri, static (stream, options, cancellation) => JsonSerializer.DeserializeAsync(stream, options.type, options.options ?? JsonSerializerOptions.Web, cancellation), (type, options), cancellationToken);
 
        [RequiresUnreferencedCode(HttpContentJsonExtensions.SerializationUnreferencedCodeMessage)]
        [RequiresDynamicCode(HttpContentJsonExtensions.SerializationDynamicCodeMessage)]
        private static Task<TValue?> FromJsonAsyncCore<TValue>(Func<HttpClient, Uri?, CancellationToken, Task<HttpResponseMessage>> getMethod, HttpClient client, Uri? requestUri, JsonSerializerOptions? options, CancellationToken cancellationToken = default) =>
            FromJsonAsyncCore(getMethod, client, requestUri, static (stream, options, cancellation) => JsonSerializer.DeserializeAsync<TValue>(stream, options ?? JsonSerializerOptions.Web, cancellation), options, cancellationToken);
 
        private static Task<object?> FromJsonAsyncCore(Func<HttpClient, Uri?, CancellationToken, Task<HttpResponseMessage>> getMethod, HttpClient client, Uri? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default) =>
            FromJsonAsyncCore(getMethod, client, requestUri, static (stream, options, cancellation) => JsonSerializer.DeserializeAsync(stream, options.type, options.context, cancellation), (type, context), cancellationToken);
 
        private static Task<TValue?> FromJsonAsyncCore<TValue>(Func<HttpClient, Uri?, CancellationToken, Task<HttpResponseMessage>> getMethod, HttpClient client, Uri? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken) =>
            FromJsonAsyncCore(getMethod, client, requestUri, static (stream, options, cancellation) => JsonSerializer.DeserializeAsync(stream, options, cancellation), jsonTypeInfo, cancellationToken);
 
        private static Task<TValue?> FromJsonAsyncCore<TValue, TJsonOptions>(
            Func<HttpClient, Uri?, CancellationToken, Task<HttpResponseMessage>> getMethod,
            HttpClient client,
            Uri? requestUri,
            Func<Stream, TJsonOptions, CancellationToken, ValueTask<TValue?>> deserializeMethod,
            TJsonOptions jsonOptions,
            CancellationToken cancellationToken)
        {
            if (client is null)
            {
                throw new ArgumentNullException(nameof(client));
            }
 
            TimeSpan timeout = client.Timeout;
 
            // Create the CTS before the initial SendAsync so that the SendAsync counts against the timeout.
            CancellationTokenSource? linkedCTS = null;
            if (timeout != Timeout.InfiniteTimeSpan)
            {
                linkedCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
                linkedCTS.CancelAfter(timeout);
            }
 
            // We call SendAsync outside of the async Core method to propagate exception even without awaiting the returned task.
            Task<HttpResponseMessage> responseTask;
            try
            {
                // Intentionally using cancellationToken instead of the linked one here as HttpClient will enforce the Timeout on its own for this part
                responseTask = getMethod(client, requestUri, cancellationToken);
            }
            catch
            {
                linkedCTS?.Dispose();
                throw;
            }
 
            bool usingResponseHeadersRead = !ReferenceEquals(getMethod, s_deleteAsync);
 
            return Core(client, responseTask, usingResponseHeadersRead, linkedCTS, deserializeMethod, jsonOptions, cancellationToken);
 
            static async Task<TValue?> Core(
                HttpClient client,
                Task<HttpResponseMessage> responseTask,
                bool usingResponseHeadersRead,
                CancellationTokenSource? linkedCTS,
                Func<Stream, TJsonOptions, CancellationToken, ValueTask<TValue?>> deserializeMethod,
                TJsonOptions jsonOptions,
                CancellationToken cancellationToken)
            {
                try
                {
                    using HttpResponseMessage response = await responseTask.ConfigureAwait(false);
                    response.EnsureSuccessStatusCode();
 
                    try
                    {
                        using Stream readStream = await GetHttpResponseStreamAsync(client, response, usingResponseHeadersRead, cancellationToken)
                            .ConfigureAwait(false);
 
                        return await deserializeMethod(readStream, jsonOptions, linkedCTS?.Token ?? cancellationToken).ConfigureAwait(false);
                    }
                    catch (OperationCanceledException oce) when ((linkedCTS?.Token.IsCancellationRequested == true) && !cancellationToken.IsCancellationRequested)
                    {
                        // Matches how HttpClient throws a timeout exception.
                        string message = SR.Format(SR.net_http_request_timedout, client.Timeout.TotalSeconds);
#if NET
                        throw new TaskCanceledException(message, new TimeoutException(oce.Message, oce), oce.CancellationToken);
#else
                        throw new TaskCanceledException(message, new TimeoutException(oce.Message, oce));
#endif
                    }
                }
                finally
                {
                    linkedCTS?.Dispose();
                }
            }
        }
 
        private static Uri? CreateUri(string? uri) =>
            string.IsNullOrEmpty(uri) ? null : new Uri(uri, UriKind.RelativeOrAbsolute);
 
        private static ValueTask<Stream> GetHttpResponseStreamAsync(
            HttpClient client,
            HttpResponseMessage response,
            bool usingResponseHeadersRead,
            CancellationToken cancellationToken)
        {
            Debug.Assert(client.MaxResponseContentBufferSize is > 0 and <= int.MaxValue);
            int contentLengthLimit = (int)client.MaxResponseContentBufferSize;
 
            if (response.Content.Headers.ContentLength is long contentLength && contentLength > contentLengthLimit)
            {
                LengthLimitReadStream.ThrowExceededBufferLimit(contentLengthLimit);
            }
 
            ValueTask<Stream> task = HttpContentJsonExtensions.GetContentStreamAsync(response.Content, cancellationToken);
 
            // If ResponseHeadersRead wasn't used, HttpClient will have already buffered the whole response upfront.
            // No need to check the limit again.
            return usingResponseHeadersRead ? GetLengthLimitReadStreamAsync(client, task) : task;
        }
 
        private static async ValueTask<Stream> GetLengthLimitReadStreamAsync(HttpClient client, ValueTask<Stream> task)
        {
            Stream contentStream = await task.ConfigureAwait(false);
            return new LengthLimitReadStream(contentStream, (int)client.MaxResponseContentBufferSize);
        }
    }
}