aspnetcore: At random intervals the application fails to set RouteValues
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
At random intervals (6-24 hours) the application fails to set RouteValues, which raises an exception. Parallel or subsequent requests completes successfully, it only happens once in a blue moon.
Technical details: .NET 6.0.5 Windows Server 2019, IIS 10 YARP (Reverse Proxy) 1.1.0 Application Insights 2.20.0
The bug happened with Ocelot as well as YARP, so i don’t think either of them is the reason.
System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.AspNetCore.Http.DefaultHttpRequest.set_RouteValues(RouteValueDictionary value)
at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.MatchAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync()
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Warning"
}
},
"Serilog": {
"Using": [ "Serilog.Sinks.File", "Serilog.Sinks.ApplicationInsights" ],
"MinimumLevel": "Warning",
"WriteTo": [
{
"Name": "Async",
"Args": {
"configure": [
{
"Name": "File",
"Args": {
"path": "Logs/serilog.txt",
"rollingInterval": "Day",
"rollOnFileSizeLimit": true,
"retainedFileCountLimit": 20,
"fileSizeLimitBytes": 104857600
}
}
]
}
},
{
"Name": "ApplicationInsights",
"Args": {
"InstrumentationKey": "xxx",
"restrictedToMinimumLevel": "Information",
"telemetryConverter": "Serilog.Sinks.ApplicationInsights.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
},
"AllowedHosts": "*",
"ConnectionStrings": {
"CmsAdmin": "Data Source=xxx;User ID=xxx;Password=xxx;Initial Catalog=xxx;MultipleActiveResultSets=True"
},
"InternalIPWhiteList": [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"::1",
"127.0.0.1"
],
"ApplicationInsights": {
"ConnectionString": "xxx",
"DeveloperMode": false,
"EnableRequestTrackingTelemetryModule": true,
"EnableAdaptiveSampling": true,
"EnableActiveTelemetryConfigurationSetup": true
},
"ReverseProxy": {
"Routes": {
"route1": {
"ClusterId": "cluster1",
"AuthorizationPolicy": "default",
"Match": {
"Path": "{**catch-all}"
},
"AllowAnonymous": false
}
},
"Clusters": {
"cluster1": {
"Destinations": {
"destination1": {
"Address": "http://192.168.197.13"
},
"destination2": {
"Address": "http://192.168.197.14"
}
}
}
},
"LoadBalancingPolicy": "PowerOfTwoChoices"
}
}
Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using xx.Api.Gateway.Services;
using xx.Api.Handlers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Logging;
using Serilog;
using Yarp.ReverseProxy.Transforms;
namespace xx.Api.Gateway
{
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
_env = env;
}
public IConfiguration Configuration { get; }
private readonly IWebHostEnvironment _env;
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddOptions();
services.AddMemoryCache();
services.AddHttpContextAccessor();
// configure basic authentication
services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
// configure DI for application services
services.AddScoped<IAccountService, AccountService>();
services.AddApplicationInsightsTelemetry();
services.AddSingleton<IPService>();
var proxyBuilder = services.AddReverseProxy();
// Initialize the reverse proxy from the "ReverseProxy" section of configuration
proxyBuilder.LoadFromConfig(Configuration.GetSection("ReverseProxy"));
proxyBuilder.AddTransforms(builderContext =>
{
// Conditionally add a transform for routes that require auth.
if (!string.IsNullOrEmpty(builderContext.Route.AuthorizationPolicy))
{
builderContext.AddRequestTransform(transformContext =>
{
var user = transformContext.HttpContext.User;
var version = user.Claims.FirstOrDefault(x => x.Type.Equals("ApiVersion"))?.Value;
var hostname = user.Claims.FirstOrDefault(x => x.Type.Equals("ApiHostname"))?.Value;
var urlVersion = user.Claims.FirstOrDefault(x => x.Type.Equals("ApiUrlVersion"))?.Value;
var environment = _env.EnvironmentName == "Production" ? "" : "-development";
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(hostname) && !string.IsNullOrEmpty(urlVersion))
{
transformContext.ProxyRequest.Headers.Remove("X-Api-Version");
transformContext.ProxyRequest.Headers.Remove("X-Api-Hostname");
transformContext.ProxyRequest.Headers.Add("X-Api-Version", version);
transformContext.ProxyRequest.Headers.Add("X-Api-Hostname", hostname);
transformContext.Path = string.Format("/v{0}{1}{2}", urlVersion, environment, transformContext.Path);
}
return default;
});
}
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// global cors policy
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
//app.UseHttpsRedirection();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapReverseProxy();
});
}
}
}
BasicAuthenticationHandler:
using System;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using xx.Api.Gateway.Entities;
using xx.Api.Gateway.Services;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Logging;
using Microsoft.ApplicationInsights;
namespace xx.Api.Handlers
{
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IAccountService _userService;
private readonly IPService _ipService;
private readonly TelemetryClient _telemetry;
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IAccountService userService,
IPService ipService,
TelemetryClient telemetry)
: base(options, logger, encoder, clock)
{
_userService = userService;
_ipService = ipService;
_telemetry = telemetry;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var remoteIp = Request.HttpContext.Connection.RemoteIpAddress;
Logger.LogInformation(remoteIp != null ? "Remote ip is: " + remoteIp.ToString() : "Remote ip is null");
if (Request.Headers.ContainsKey("Authorization"))
{
Logger.LogDebug("Logging in with autorization");
return await AuthenticateWithAuthorization();
}
if(remoteIp != null && _ipService.IsIPWhiteListed(remoteIp))
{
Logger.LogDebug("Logging in with hostname");
return await AuthenticateWithApiHost();
}
return AuthenticateResult.Fail("Unknown authentication method.");
}
private async Task<AuthenticateResult> AuthenticateWithApiHost()
{
Account account = null;
try
{
string apiHost;
if(Request.Headers.ContainsKey("X-Api-Hostname"))
{
apiHost = Request.Headers["X-Api-Hostname"];
Logger.LogInformation("X-Api-Hostname " + apiHost);
}
else
{
var hosts = Request.Host.Host.Split(".");
if (hosts.Length < 3)
{
return AuthenticateResult.Fail("No hostname");
}
apiHost = hosts[0];
Logger.LogInformation("Parsed hostname " + apiHost);
}
account = await _userService.AuthenticateHost(apiHost);
}
catch(Exception ex)
{
_telemetry.TrackException(ex);
return AuthenticateResult.Fail("Invalid hostname");
}
if (account == null)
return AuthenticateResult.Fail("Invalid hostname");
return CreateTicket(account);
}
protected async Task<AuthenticateResult> AuthenticateWithAuthorization()
{
Account account = null;
try
{
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
var username = credentials[0];
var password = credentials[1];
if(Request.Headers.ContainsKey("X-Api-Verify-Token"))
{
account = await _userService.AuthenticateAccessToken(username, password);
}
else
{
account = await _userService.Authenticate(username, password);
}
}
catch(Exception ex)
{
_telemetry.TrackException(ex);
return AuthenticateResult.Fail("Invalid Authorization Header");
}
if (account == null)
return AuthenticateResult.Fail("Invalid Username or Password");
return CreateTicket(account);
}
protected AuthenticateResult CreateTicket(Account account)
{
Version apiVersion = new Version(1,0);
int urlVersion = 2;
if(Request.Headers.ContainsKey("X-Api-Version"))
{
Version v;
if(Version.TryParse(Request.Headers["X-Api-Version"].ToString(), out v))
{
urlVersion = v.Major > 1 ? v.Major : 2;
apiVersion = v;
}
}
Logger.LogInformation(string.Format("Api hostname: {0}, Api version: {1}, Url version: {2}", account.ApiHost, apiVersion, urlVersion));
var name = string.IsNullOrEmpty(account.Username) ? account.ApiHost : account.Username;
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, account.Id.ToString()),
new Claim(ClaimTypes.Name, name),
new Claim("ApiHostname", account.ApiHost),
new Claim("ApiUrlVersion", urlVersion.ToString()),
new Claim("ApiVersion", apiVersion.ToString())
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
Expected Behavior
No exception
Steps To Reproduce
Sadly i can’t reproduce it, since i don’t know what’s actually wrong.
Exceptions (if any)
System.NullReferenceException: Object reference not set to an instance of an object. at Microsoft.AspNetCore.Http.DefaultHttpRequest.set_RouteValues(RouteValueDictionary value) at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.MatchAsync(HttpContext httpContext) at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync()
.NET Version
6.0.5
Anything else?
.NET 6.0.5 Windows Server 2019, IIS 10 YARP (Reverse Proxy) 1.1.0 Application Insights 2.20.0
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 44 (20 by maintainers)
I didn’t even realize the problem was right in front of my eyes. @okolvik-avento Thanks for the dumps, the issue in the in SqlCommand implementation here
When you run an async sql command, even when you await it properly, the implementation fires the diagnostics listener callback after running the user’s continuation. This allows the telemetry initializers in app insights to run concurrently with the request, causing this chaos.
This isn’t an app insights issue (this time 😄), it turns out.
cc @ajcvickers @roji
Thanks for the great debugging and dumps @okolvik-avento !
Thank you for the quick response @davidfowl!
This is fixed!
Yes, the problematic code exists in both implementations 😄, see https://github.com/dotnet/SqlClient/issues/1634
First thing i see is a huge performance increase. While tests with System.Data.SqlClient had 20ms responsetime with max up to 100ms, i now see sub 5ms responsetimes with max up to 20ms
BasicAuthenticationHandler looks fine. Async functions are ok so long as you
awaitthem so the flow stays linear.Yeah i’m running the debug diagnostic tool on the application pool