question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

kestrel .net5 dateHeaderValues is null

See original GitHub issue

Hi,

I’ve been breaking my head on this issue for 2 days now, I followed the sample on IdentityModel.OidcClient.Samples to implement a local server to handle the oidc callback page. but I keep running into the issue of a null reference exception inside kestrel itself. Having delved deeper into this issue I have identified the source to be on aspnetcore/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs:1188. Looking into the call I have a suspicion it is “simply” a racing condition. since I have modified the sample a bit I’ll post it below.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using IdentityModel.OidcClient;
using IdentityModel.OidcClient.Browser;
using IdentityModel.OidcClient.Results;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Serilog;
using Serilog.Sinks.SystemConsole.Themes;

namespace Fyn.Windows.Service
{
    public static class Authentication
    {
        private static readonly String Authority = "https://unifyned.cloud";
        private static readonly String Api = "https://unifyned.cloud/v1/cms/file";

        private static OidcClient? oidcClient;
        private static HttpClient apiClient = new HttpClient { BaseAddress = new Uri(Api), DefaultRequestVersion = new Version(2, 0) };

        public static async ValueTask Signin()
        {
            SystemBrowser browser = new SystemBrowser(5002);
            String redirectUri = $"http://127.0.0.1:{browser.Port}";

            OidcClientOptions options = new OidcClientOptions
            {
                Authority = Authority,
                ClientId = "Shell.Windows",
                RedirectUri = redirectUri,
                Scope = "openid profile email",
                FilterClaims = false,

                Browser = browser,
                IdentityTokenValidator = new JwtHandlerIdentityTokenValidator(),
                RefreshTokenInnerHttpHandler = new HttpClientHandler(),
            };

            options.LoggerFactory.AddSerilog(
                new LoggerConfiguration()
                    .MinimumLevel.Debug()
                    .Enrich.FromLogContext()
                    .WriteTo.Console(
                        outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}", 
                        theme: AnsiConsoleTheme.Code
                    )
                    .CreateLogger()
            );

            oidcClient = new OidcClient(options);
            LoginResult? result = await oidcClient.LoginAsync();

            apiClient = new HttpClient(result.RefreshTokenHandler)
            {
                BaseAddress = new Uri(Api),
            };

            result.Show();

            await result.NextSteps();
        }

        private static void Show(this LoginResult result)
        {
            if (result.IsError)
            {
                Console.WriteLine($"\n\nError:\n{result.Error}");

                return;
            }

            Console.WriteLine("\n\nClaims:");

            foreach (Claim claim in result.User.Claims)
            {
                Console.WriteLine($"{claim.Type}: {claim.Value}");
            }

            Dictionary<String, JsonElement>? values = JsonSerializer.Deserialize<Dictionary<String, JsonElement>>(result.TokenResponse.Raw);

            Console.WriteLine("token response...");

            if (values == null)
            {
                return;
            }

            foreach ((String key, JsonElement value) in values)
            {
                Console.WriteLine($"{key}: {value}");
            }
        }

        private static async ValueTask NextSteps(this LoginResult result)
        {
            String currentAccessToken = result.AccessToken;
            String currentRefreshToken = result.RefreshToken;

            String menu = "  x...exit  c...call api   ";

            if (currentRefreshToken != null)
            {
                menu += "r...refresh token   ";
            }

            while (true)
            {
                Console.WriteLine("\n\n");

                Console.Write(menu);
                ConsoleKeyInfo key = Console.ReadKey();

                switch (key.Key)
                {
                    case ConsoleKey.X:
                    {
                        return;
                    }

                    case ConsoleKey.C:
                    {
                        await CallApi();
                        break;
                    }

                    case ConsoleKey.R:
                    {
                        RefreshTokenResult refreshResult = await oidcClient.RefreshTokenAsync(currentRefreshToken);
                        if (refreshResult.IsError)
                        {
                            Console.WriteLine($"Error: {refreshResult.Error}");
                        }
                        else
                        {
                            currentRefreshToken = refreshResult.RefreshToken;
                            currentAccessToken = refreshResult.AccessToken;

                            Console.WriteLine("\n\n");
                            Console.WriteLine($"access token:   {currentAccessToken}");
                            Console.WriteLine($"refresh token:  {currentRefreshToken ?? "none"}");
                        }

                        break;
                    }
                }
            }
        }

        private static async ValueTask CallApi()
        {
            HttpResponseMessage response = await apiClient.GetAsync("");

            if (response.IsSuccessStatusCode)
            {
                JsonDocument json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
                Console.WriteLine("\n\n");
                Console.WriteLine(json.RootElement);
            }
            else
            {
                Console.WriteLine($"Error: {response.ReasonPhrase}");
            }
        }
    }

    public class SystemBrowser : IBrowser
    {
        public Int32 Port { get; }
        private readonly String _path;

        public SystemBrowser(Int32? port = null, String? path = null)
        {
            _path = path;
            Port = port ?? GetRandomUnusedPort();
        }

        private static Int32 GetRandomUnusedPort()
        {
            TcpListener listener = new TcpListener(IPAddress.Loopback, 0);
            listener.Start();

            Int32 port = ((IPEndPoint)listener.LocalEndpoint).Port;
            listener.Stop();

            return port;
        }

        public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken)
        {
            await using LoopbackHttpListener listener = new LoopbackHttpListener(Port, _path);
            await listener.Start();

            OpenBrowser(options.StartUrl);

            try
            {
                String? result = await listener.WaitForCallbackAsync();

                return String.IsNullOrWhiteSpace(result) 
                    ? new BrowserResult
                    {
                        ResultType = BrowserResultType.UnknownError, 
                        Error = "Empty response.",
                    } 
                    : new BrowserResult
                    {
                        ResultType = BrowserResultType.Success, 
                        Response = result,
                    };
            }
            catch (TaskCanceledException ex)
            {
                return new BrowserResult
                {
                    ResultType = BrowserResultType.Timeout, 
                    Error = ex.Message,
                };
            }
            catch (Exception ex)
            {
                return new BrowserResult
                {
                    ResultType = BrowserResultType.UnknownError, 
                    Error = ex.Message,
                };
            }
        }
        public static void OpenBrowser(String url)
        {
            // hack because of this: https://github.com/dotnet/corefx/issues/10361
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                url = url.Replace("&", "^&");
                Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                Process.Start("xdg-open", url);
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                Process.Start("open", url);
            }
            else
            {
                Process.Start(url);
            }
        }
    }

    public class LoopbackHttpListener : IAsyncDisposable
    {
        const Int32 DefaultTimeout = 300_000;

        private readonly IWebHost _host;
        private readonly TaskCompletionSource<String> _source = new TaskCompletionSource<String>();

        public LoopbackHttpListener(Int32 port, String? path = null)
        {
            _host = new WebHostBuilder()
                .UseUrls($"http://127.0.0.1:{port}/{path?.TrimStart('/')}")
                .UseKestrel()
                .Configure(builder =>
                {
                    builder.Run(async context =>
                    {
                        switch (context.Request.Method)
                        {
                            case "GET":
                            {
                                await SetResult(context.Request.QueryString.Value, context);
                                break;
                            }

                            case "POST" when !context.Request.ContentType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase):
                            {
                                context.Response.StatusCode = 415;
                                break;
                            }

                            case "POST":
                            {
                                using StreamReader sr = new StreamReader(context.Request.Body, Encoding.UTF8);
                                await SetResult(await sr.ReadToEndAsync(), context);
                                break;
                            }

                            default:
                            {
                                context.Response.StatusCode = 405;
                                break;
                            }
                        }
                    });
                })
                .ConfigureLogging(options =>
                {
                    options.AddSerilog(
                        new LoggerConfiguration()
                            .MinimumLevel.Debug()
                            .Enrich.FromLogContext()
                            .WriteTo.Console(
                                outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}",
                                theme: AnsiConsoleTheme.Code
                            )
                            .CreateLogger()
                    );
                })
                .Build();
        }

        public Task Start()
        {
            return _host.StartAsync();
        }

        public async ValueTask DisposeAsync()
        {
            await Task.Delay(500);

            _host.Dispose();
        }

        private async ValueTask SetResult(String value, HttpContext context)
        {
            try
            {
                context.Response.StatusCode = 200;
                context.Response.ContentType = "text/html";
                await context.Response.WriteAsync("<h1>You can now return to the application.</h1>");
                await context.Response.Body.FlushAsync();

                _source.TrySetResult(value);
            }
            catch(Exception exception)
            {
                context.Response.StatusCode = 400;
                context.Response.ContentType = "text/html";
                await context.Response.WriteAsync("<h1>Invalid request.</h1>");

#if DEBUG
                await context.Response.WriteAsync($"<p>{exception.Message}</p>");
                await context.Response.WriteAsync($"<p>{exception.StackTrace}</p>");
#endif

                await context.Response.Body.FlushAsync();
            }
        }

        public async ValueTask<String> WaitForCallbackAsync(Int32 timeout = DefaultTimeout)
        {
            await Task.Delay(timeout);

            _source.TrySetCanceled();

            return await _source.Task;
        }
    }
}

It is the this line which triggers the exception

await context.Response.WriteAsync("<h1>You can now return to the application.</h1>");

Am I just an idiot and blind for a missing await somewhere? Is the Heartbeat in kestrel internally broken? Is my config of the WebHost correct?

Thank you in advance!

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:16 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
ilharpcommented, Dec 18, 2020

Wooooooooooooo

I’ve been breaking my head on this issue for 2 days now

exactly the same


One difference is that I try to run multiple Hosts while using PackageReference in a single Console app. The problem disappears after modified to FrameworkReference. (Commit)

  <ItemGroup>
+    <FrameworkReference Include="Microsoft.AspNetCore.App" />
-    <PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Http.Connections" Version="1.1.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
  </ItemGroup>
Read more comments on GitHub >

github_iconTop Results From Across the Web

asp.net - Net7 C# Kestrel UseHttps() throws System. ...
Net7 C# Kestrel UseHttps() throws System.ArgumentNullException:Value cannot be null. (Parameter 'provider') when attempting to enable QUIC/Http3.
Read more >
Untitled
This package provides AspNetCore Kestrel based communication listener for ... WebNov 24, 2020 · kestrel .net5 dateHeaderValues is null · Issue #28112 ...
Read more >
Configure options for the ASP.NET Core Kestrel web server
Learn about configuring options for Kestrel, the cross-platform web server for ASP. ... MinDataRate to null , even for an HTTP/2 request.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found