HttpContext on async methods

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.

Is this supposed to work?

@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.

I’d recommend avoiding the singleton where ever possible and instead resolve the HttpContext from:

var httpCtx = ((AspNetRequest)Request).HttpRequest.ToHttpContextBase();

In v6.3 (now released) this is available from:

var httpCtx = Request.ToHttpContextBase();

You’ll need to either change IService to Service or have your Service implement IRequiresRequest so the Request can be injected into your Service.

@mythz I’m with you here. I can work on removing references to the singleton on our code, but in the particular case that’s bothering us, the reference is on https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.Razor/Html/AntiXsrf/AntiForgery.cs

And since AntiForgeryWorker is internal sealed, we can’t even work directly with it.

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.

Thanks, I can look into that later.

I was able to track down our issue a bit better. A repo is at https://github.com/brunomlopes/servicestack-async-global-response-httpcontext-issue

On a GlobalResponseFilters, HttpContext:Current is null when a service endpoint is async and is called via /json/reply/

I’ve added the core of the code below.
this works: GET http://localhost:62577/hello/john
this fails: GET http://localhost:62577/json/reply/Hello?Name=john

Should both work?

    /// <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.

var httpCtx = Request.ToHttpContextBase();

Many thanks, I think that unblocks our async scenarios!

We’ll be inspecting and changing those instances where we access the singleton, but until then it’s working.