abp icon indicating copy to clipboard operation
abp copied to clipboard

Blazor server side better error UI

Open hikalkan opened this issue 3 years ago • 11 comments

We'd implemented a custom logger to show errors for Blazor WASM. For server side, global exception handling is not possible yet (see https://github.com/dotnet/aspnetcore/issues/30940). However, we can still show a better messagebox (modal dialog) instead of the yellow bottom bar, for blazor server side. We can provide an option to the user to refresh the page in this modal dialog (message modal can have two options: Close & Refresh Page). However, this should be carefully implemented. Because, if we handle every error and try to show message, it won't work. We should only handle the errors for blazor application's page, not other HTTP requests and MVC page requests.

hikalkan avatar Mar 25 '21 05:03 hikalkan

Good day, this fix gonna be available on version 5.4?

davidjhurtado avatar Apr 28 '22 18:04 davidjhurtado

Good day, this fix gonna be available on version 5.4?

Yes, we expect it to be available on version 6.0.

berkansasmaz avatar May 05 '22 06:05 berkansasmaz

Any updates on this one?

beriniwlew avatar Mar 24 '23 04:03 beriniwlew

There have been investigations, but the results were unsatisfactory due to some limitations. However, we will close the problem once we have found a satisfactory solution.

berkansasmaz avatar Mar 30 '23 06:03 berkansasmaz

So what's the workaround so far? Catching the exception in the component's code on the Blazor side?

If so, how do I open the exception modal with the message?

beriniwlew avatar Mar 30 '23 06:03 beriniwlew

Yes, we try to ensure that there are no unhandled exceptions as mentioned in Microsoft's documentation.

By the way, you can see the work done within the scope of this issue here.

berkansasmaz avatar Mar 30 '23 07:03 berkansasmaz

Abp 7.3.3 Dotnet core 7

It can be done easily by creating the ComponentBase from scratch just like the original (github):

in blazor components, every events will be called from Task IHandleEvent.HandleEventAsync so if it can be override, then we can create a handler based on the Task.Status

  1. Create the MyComponentBase:

public abstract class MyComponentBase : IComponent, IHandleEvent, IHandleAfterRender
{
    ...

    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
    {
        var task = callback.InvokeAsync(arg);
        var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
            task.Status != TaskStatus.Canceled;

        // After each event, we synchronously re-render (unless !ShouldRender())
        // This just saves the developer the trouble of putting "StateHasChanged();"
        // at the end of every event callback.
        StateHasChanged();

        return shouldAwaitTask ?
            CallStateHasChangedOnAsyncCompletion(task) :
            Task.CompletedTask;
    }

    ...

}

Then task will be run in CallStateHasChangedOnAsyncCompletion method:


    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch // avoiding exception filters for AOT runtime support
        {
            // Ignore exceptions from task cancellations, but don't bother issuing a state change.
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        StateHasChanged();
    }

  1. Instead of throw call a method for handle the task.Exception named OnCallStateHasChangedOnAsyncCompletionExceptionAsync:
protected virtual Task OnCallStateHasChangedOnAsyncCompletionExceptionAsync(AggregateException? exception)
=> Task.CompletedTask;

Make sure to set the virtual keyword to allow override this method.

  1. Now CallStateHasChangedOnAsyncCompletion would be like this:
    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch // avoiding exception filters for AOT runtime support
        {
            // Ignore exceptions from task cancellations, but don't bother issuing a state change.
            if (task.IsCanceled)
            {
                return;
            }

            await OnCallStateHasChangedOnAsyncCompletionExceptionAsync(task.Exception);
        }

        StateHasChanged();
    }
  1. From OwningComponentBase and AbpComponentBase rewrite it to MyOwningComponentBase and MyAbpComponentBase.

  2. In MyAbpComponentBase, the OnCallStateHasChangedOnAsyncCompletionExceptionAsync method can be override like this:

    protected override Task OnCallStateHasChangedOnAsyncCompletionExceptionAsync(AggregateException? exception)
    {
        return HandleErrorAsync(exception);
    }

HandleErrorAsync is a method in MyAbpComponentBase that appear a friendly modal message.

In the RunInitAndSetParametersAsync method also can do the same for handle errors.

    private async Task RunInitAndSetParametersAsync()
    {
        try
        {
            ...
        }
        catch (Exception ex)
        {
            await OnRunInitAndSetParametersExceptionAsync(ex);
        }

    }

    public virtual Task OnRunInitAndSetParametersExceptionAsync(Exception ex) => Task.CompletedTask;

In MyAbpComponentBase:

    public override Task OnRunInitAndSetParametersExceptionAsync(Exception ex)
    {
        return HandleErrorAsync(ex);
    }

With Regards

BorzoNosrati avatar Oct 13 '23 10:10 BorzoNosrati

I bumped into it in ABP 7.4.0 commercial: Unhandled exception rendering component: Forbidden Volo.Abp.Http.Client.AbpRemoteCallException

rushasdev avatar Oct 23 '23 13:10 rushasdev

I bumped into it in ABP 7.4.0 commercial: Unhandled exception rendering component: Forbidden Volo.Abp.Http.Client.AbpRemoteCallException

Please check the logs of the backend app, and create a new issue. Thanks.

maliming avatar Oct 24 '23 00:10 maliming

Abp 7.3.3 Dotnet core 7

It can be done easily by creating the ComponentBase from scratch just like the original (github):

in blazor components, every events will be called from Task IHandleEvent.HandleEventAsync so if it can be override, then we can create a handler based on the Task.Status

  1. Create the MyComponentBase:
public abstract class MyComponentBase : IComponent, IHandleEvent, IHandleAfterRender
{
    ...

    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
    {
        var task = callback.InvokeAsync(arg);
        var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
            task.Status != TaskStatus.Canceled;

        // After each event, we synchronously re-render (unless !ShouldRender())
        // This just saves the developer the trouble of putting "StateHasChanged();"
        // at the end of every event callback.
        StateHasChanged();

        return shouldAwaitTask ?
            CallStateHasChangedOnAsyncCompletion(task) :
            Task.CompletedTask;
    }

    ...

}

Then task will be run in CallStateHasChangedOnAsyncCompletion method:

    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch // avoiding exception filters for AOT runtime support
        {
            // Ignore exceptions from task cancellations, but don't bother issuing a state change.
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        StateHasChanged();
    }
  1. Instead of throw call a method for handle the task.Exception named OnCallStateHasChangedOnAsyncCompletionExceptionAsync:
protected virtual Task OnCallStateHasChangedOnAsyncCompletionExceptionAsync(AggregateException? exception)
=> Task.CompletedTask;

Make sure to set the virtual keyword to allow override this method.

  1. Now CallStateHasChangedOnAsyncCompletion would be like this:
    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch // avoiding exception filters for AOT runtime support
        {
            // Ignore exceptions from task cancellations, but don't bother issuing a state change.
            if (task.IsCanceled)
            {
                return;
            }

            await OnCallStateHasChangedOnAsyncCompletionExceptionAsync(task.Exception);
        }

        StateHasChanged();
    }
  1. From OwningComponentBase and AbpComponentBase rewrite it to MyOwningComponentBase and MyAbpComponentBase.
  2. In MyAbpComponentBase, the OnCallStateHasChangedOnAsyncCompletionExceptionAsync method can be override like this:
    protected override Task OnCallStateHasChangedOnAsyncCompletionExceptionAsync(AggregateException? exception)
    {
        return HandleErrorAsync(exception);
    }

HandleErrorAsync is a method in MyAbpComponentBase that appear a friendly modal message.

In the RunInitAndSetParametersAsync method also can do the same for handle errors.

    private async Task RunInitAndSetParametersAsync()
    {
        try
        {
            ...
        }
        catch (Exception ex)
        {
            await OnRunInitAndSetParametersExceptionAsync(ex);
        }

    }

    public virtual Task OnRunInitAndSetParametersExceptionAsync(Exception ex) => Task.CompletedTask;

In MyAbpComponentBase:

    public override Task OnRunInitAndSetParametersExceptionAsync(Exception ex)
    {
        return HandleErrorAsync(ex);
    }

With Regards



i noticed some information being missed, for example  , the ```StateHasChanged()``` method does not exist in the custom component base , also , the ```HandleErrorAsync()``` method  is only available if you inherits from ```AbpComponentBase```

i quit liked the solution but the implementation needs more detail.
also  , i think it's time for @Abp to think about a solution to handle errors generally.

Thanks.

pouyababaie avatar Mar 04 '24 15:03 pouyababaie

Any progress on this one?

rcalv002 avatar May 08 '24 19:05 rcalv002