kestrel .net5 dateHeaderValues is null
See original GitHub issueHi,
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:
- Created 3 years ago
- Reactions:1
- Comments:16 (10 by maintainers)
Wooooooooooooo
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 toFrameworkReference
. (Commit)I’d recommend reading this https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-5.0&tabs=visual-studio#framework-reference