We’re moving some code to async methods, and we’ve noticed an issue with AntiForgery usage, which accesses HttpContext.Current.
On async methods, on ServiceStack 6.2.0, it seems HttpContext.Current can be null.
A small repro of the main issue on a .net 4.8 project:
public class HelloService : IService
{
public async Task<HelloResponse> Any(Hello request)
{
await Task.Delay(TimeSpan.FromMilliseconds(250));
// This blows up because HttpContext.Current is null
return new HelloResponse { Result = $"Hello, {request.Name ?? "John Doe"}. ContextContains: {HttpContext.Current.User?.Identity}"};
}
public HelloResponse Any(HelloSync request)
{
Thread.Sleep(TimeSpan.FromMilliseconds(250));
return new HelloResponse { Result = $"Hello, {request.Name ?? "John Doe"}. ContextContains: {HttpContext.Current.User?.Identity}"};
}
}
While we understand that usage of HttpContext is to be avoided, in the case of AntiForgery, we don’t see a way around it.
@brunomlopes I’ve tried reproducing the error without success, but as you suggested, working with HttpContext.Current and potential other threads can cause issues.
Can you try accessing the Request as an AspNetRequest to access the HttpContext instance? Eg
return new HelloResponse { Result = $"Hello, {request.Name ?? "John Doe"}. ContextContains: {((AspNetRequest)Request).HttpRequest.ToHttpContextBase().User?.Identity}"};
If you can share a project on GitHub which reproduces the issue for that, I will test that, but following the steps of creating a new NetFx project from our Start page and adding your Hello service I wasn’t able to reproduce the problem on my Windows 10 with IIS Express. Is this something that only occurs when hosted with IIS (non express)?
Thanks for the example, my repro was missing a <httpRuntime targetFramework="4.8"/>. I think I still have this issue on filters, but I’m going to try and get another small repro.
That’s how Razor MVC had it which would’ve been copied verbatim, I’m assuming it’s because that class wasn’t meant to be used directly. But if you can send a PR with all the classes you want public and any other changes you need, I can merge it.
/// <summary>
/// Define your ServiceStack web service request (i.e. the Request DTO).
/// </summary>
[Route("/hello")]
[Route("/hello/{name}")]
public class Hello :IReturn<HelloResponse>
{
public string Name { get; set; }
}
[Route("/hello-sync")]
[Route("/hello-sync/{name}")]
public class HelloSync :IReturn<HelloResponse>
{
public string Name { get; set; }
}
public class HelloResponse
{
public string Result { get; set; }
}
public class HelloService : IService
{
public async Task<HelloResponse> Any(Hello request)
{
await Task.Delay(TimeSpan.FromMilliseconds(250));
// This blows up because HttpContext.Current is null
return new HelloResponse { Result = $"Hello, {request.Name ?? "John Doe"}. ContextContains: {HttpContext.Current.User?.Identity}"};
}
public HelloResponse Any(HelloSync request)
{
Thread.Sleep(TimeSpan.FromMilliseconds(250));
return new HelloResponse { Result = $"Hello, {request.Name ?? "John Doe"}. ContextContains: {HttpContext.Current.User?.Identity}"};
}
}
public class Global : System.Web.HttpApplication
{
public class HelloAppHost : AppHostBase
{
public HelloAppHost() : base("Hello Web Services", typeof(HelloService).Assembly)
{
Plugins.Add(new APlugin());
}
public override void Configure(Container container)
{
container.Register((IAppHost)this);
}
}
protected void Application_Start(object sender, EventArgs e)
{
(new HelloAppHost()).Init();
}
}
public class APlugin : IPlugin
{
public void Register(IAppHost appHost)
{
// Both fail
appHost.GlobalResponseFilters.Add(SetupRequestForgeryCookies.ResponseFilter);
//appHost.GlobalResponseFiltersAsync.Add(SetupRequestForgeryCookies.ResponseFilterAsync);
}
}
public class SetupRequestForgeryCookies
{
public static void ResponseFilter(IRequest req, IResponse res, object dto)
{
if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext.Current is null");
}
public static Task ResponseFilterAsync(IRequest req, IResponse res, object dto)
{
ResponseFilter(req, res, dto);
return Task.CompletedTask;
}
}
I’m not happy with the solution as Framework libraries are supposed to await with .ConfigureAwait(continueOnCapturedContext:false) in all async await calls, but I’ve identified instances where doing this loses HttpContext.Current context so I’m only calling .ConfigureAwait(false) in these cases in .NET Core.
Anyway with this change all your API examples retains HttpContext.Current, which is now available from v6.3.1+ that’s now available on MyGet.
However given how fragile HttpContext.Current can lose context in async/await calls I’d recommend avoiding accessing the singleton and resolving the HttpContext from IRequest instead which isn’t affected from async/await hops.