Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blazor 8 - delegating handler cannot use local/session storage #55770

Closed
1 task done
AdamJachocki opened this issue May 17, 2024 · 7 comments
Closed
1 task done

Blazor 8 - delegating handler cannot use local/session storage #55770

AdamJachocki opened this issue May 17, 2024 · 7 comments
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. question Status: Resolved

Comments

@AdamJachocki
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I consider it a bug.

First of all notice that this is not DI problem, as I have already gone through ServicesAccessorCircuitHandler that fills ScopedService accessors.

Blazor 8 interactive server application. I have turned off prerendering.
In my component I call API client from OnAfterRenderAsync method like:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);
    await RefreshDocuments();
}

RefreshDocuments calls my API client which has delegating handler that is suppose to attach and refresh access token to headers:

internal class AccessTokenRefreshHandler(ITokenRefreshService _tokenRefresher,
    IScopedServiceAccessor _scopedServiceAccessor) : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        await AddAccessTokenIfNeeded(request);
       ...

Now in AddAccessTokenIfNeeded I try to get a service that dependes on browser session storage:

return _scopedServiceAccessor.Services?.GetRequiredService<IClientTokenStorage>();

And note that this works - I get the proper service.
But when I call session storage read, I get this JS exception:

"JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method."

although I called this from withing OnAfterRenderAsync.

This one makes Blazor 8 not production ready from my point of view. Unless there is some workaround for this?

Expected Behavior

Session/Local storage should be readable from DelegatingHandler

Steps To Reproduce

No response

Exceptions (if any)

JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.

.NET Version

8.0.5

Anything else?

No response

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label May 17, 2024
@javiercn
Copy link
Member

@AdamJachocki thanks for contacting us.

IJSRuntime is a scoped service. You are consuming it from inside a delegating handler, if you are using HttpClientFactory that's creating its own scope, which results in the service not being initialized.

Not sure what's going on in your app/code, but its signaling that you are getting a new instance of an uninitialized IJSRuntime.

@javiercn javiercn added the Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. label May 20, 2024
@ElderJames
Copy link
Contributor

@javiercn So what are the best practices for getting local storage when requesting with httpclient?

@AdamJachocki
Copy link
Author

OK, I found a solution but it was like the other problem. I mean, I searched it differently. You need to put scope services into DI scope of delegating handler. And it's done pretty easy.

Official solution is to use CircuitHandler: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-8.0#access-server-side-blazor-services-from-a-different-di-scope

But I had some other problems with that, so I did some other work.

My solution can use client side stuff.
I have a ApiClient like:

class MyApiClient: IMyApiClient
{
  public IUserRequests UserRequests { get; set; }

  public MyApiClient(HttpClient httpClient, IStorageService storageService)
  {
    UserRequests = new UserRequests(httpClient, storageService);
  }
}

So I can use it like that insinde Blazor client side:

[Inject] private IMyApiClient _apiClient {get; set;} = default!;

private async Task DoSomeStuff()
{
    await _apiClient.UserRequests.GetUserData();
}

Now, the real thing is that my UserRequests class inherits from BaseRequest class that looks something like that:

abstract class BaseRequest
{
    private readonly HttpClient _httpClient;
    private readonly IStorageService _storageService;
   

    public BaseRequest(HttpClient httpClient, IStorageService storageService)
    {
        //assign them
    }

    protected async Task<HttpResponseMessage> GetWithAuthorize(string endpoint)
    {
         var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
         request.Headers.Add("myapp-access-token", await _storageService.ReadAccessToken());
         request.Headers.Add("myapp-refresh-token", await _storageService.ReadRefreshToken());

         var response = await _httpClient.SendAsync(request);

         var newAccessToken = response.GetHeader("myapp-new-access-token);
         if(!string.IsNullOrEmpty(newAccessToken))
           await _storageService.WriteAccessToken(newAccessToken);

         //the same with refresh token
         return result;
    }
}

Now every Request class looks like that:

class UserRequests: BaseRequest
{
   //constructor and then:

  public async Task<Data> GetUserData()
  {
       var response = await GetWithAuthorize("/api/user/data");
       return await ResponseToData(response);
  }
}

And the real stuff is done in delegating handler:

class TokenHandler: DelegatingHandler
{
  public async Task<HttpResponseMessage> Send(HttpRequestMessage request)
  {
     //check the myapp-access-token and refresh-token from header
    //if they are present, create a authorization header with that
    //refresh token if needed
    //after refreshing token add headers to response with them
  }
}

So this is quite simple:

  1. Base Api Request class adds tokens to some headers - and it can be done on client side
  2. Delegating handler reads those headers and creates Authorize header if needed
  3. Delegating handler refreshes tokens if needed
  4. If tokens were refreshed, DelegatingHandler adds them to response headers
  5. Base Api Request reads those headers and if there are new tokens, writes them to... somewhere - this is also done on client side

That's what I came up with and it works quite good.

@dotnet-policy-service dotnet-policy-service bot added Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. and removed Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. labels May 21, 2024
@javiercn
Copy link
Member

@AdamJachocki thanks for the additional details.

I don't fully follow everything in detail, but what you've said sounds reasonable. With HttpClientFactory is about getting scoped services in the right scope. The challenge is understanding the scopes that are at play at a given point (since HttpClient creates their own).

@javiercn javiercn added question ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. and removed Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. labels May 21, 2024
@javiercn
Copy link
Member

@ElderJames what @AdamJachocki came up with, and its documented in the link he referred to.

@AdamJachocki
Copy link
Author

@AdamJachocki thanks for the additional details.

I don't fully follow everything in detail, but what you've said sounds reasonable. With HttpClientFactory is about getting scoped services in the right scope. The challenge is understanding the scopes that are at play at a given point (since HttpClient creates their own).

Official solution uses CircuitHandler to have proper scope in Delegating handler. My solution doesn't even uses this scope. It creates HttpRequestMessage on client side with proper information in temp headers. But this solution is more specific.

Copy link
Contributor

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. question Status: Resolved
Projects
None yet
Development

No branches or pull requests

4 participants
@javiercn @ElderJames @AdamJachocki and others