Skip to content

Commit 4a8e4fc

Browse files
Clean up all the stuff about detailed errors and prerendering
1 parent d673d0a commit 4a8e4fc

File tree

9 files changed

+146
-62
lines changed

9 files changed

+146
-62
lines changed

src/Components/Samples/BlazorServerApp/Shared/MainLayout.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</div>
1111

1212
<div class="content px-4">
13-
<ErrorBoundary>
13+
<ErrorBoundary AutoReset="true">
1414
@Body
1515
</ErrorBoundary>
1616
</div>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Components.Web;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Options;
9+
using Microsoft.JSInterop;
10+
11+
namespace Microsoft.AspNetCore.Components.Server.Circuits
12+
{
13+
internal class RemoteErrorBoundaryLogger : IErrorBoundaryLogger
14+
{
15+
private static readonly Action<ILogger, string, Exception> _exceptionCaughtByErrorBoundary = LoggerMessage.Define<string>(
16+
LogLevel.Warning,
17+
100,
18+
"Unhandled exception rendering component: {Message}");
19+
20+
private readonly ILogger _logger;
21+
private readonly IJSRuntime _jsRuntime;
22+
private readonly CircuitOptions _options;
23+
24+
public RemoteErrorBoundaryLogger(ILogger<ErrorBoundary> logger, IJSRuntime jsRuntime, IOptions<CircuitOptions> options)
25+
{
26+
_logger = logger;
27+
_jsRuntime = jsRuntime;
28+
_options = options.Value;
29+
}
30+
31+
public ValueTask LogErrorAsync(Exception exception, bool clientOnly)
32+
{
33+
// If the end user clicks on the UI to re-log the error, we don't want to spam the
34+
// server logs so we only re-load to the client
35+
if (!clientOnly)
36+
{
37+
// We always log detailed information to the server-side log
38+
_exceptionCaughtByErrorBoundary(_logger, exception.Message, exception);
39+
}
40+
41+
// We log to the client only if the browser is connected interactively, and even then
42+
// we may suppress the details
43+
var shouldLogToClient = (_jsRuntime as RemoteJSRuntime)?.IsInitialized == true;
44+
if (shouldLogToClient)
45+
{
46+
var message = _options.DetailedErrors
47+
? exception.ToString()
48+
: $"For more details turn on detailed exceptions in '{nameof(CircuitOptions)}.{nameof(CircuitOptions.DetailedErrors)}'";
49+
return _jsRuntime.InvokeVoidAsync("console.error", message);
50+
}
51+
else
52+
{
53+
return ValueTask.CompletedTask;
54+
}
55+
}
56+
}
57+
}

src/Components/Server/src/Circuits/RemoteJSRuntime.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ internal class RemoteJSRuntime : JSRuntime
1919

2020
public ElementReferenceContext ElementReferenceContext { get; }
2121

22+
public bool IsInitialized => _clientProxy is not null;
23+
2224
public RemoteJSRuntime(IOptions<CircuitOptions> options, ILogger<RemoteJSRuntime> logger)
2325
{
2426
_options = options.Value;

src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Components.Server.BlazorPack;
1111
using Microsoft.AspNetCore.Components.Server.Circuits;
1212
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
13+
using Microsoft.AspNetCore.Components.Web;
1314
using Microsoft.AspNetCore.SignalR.Protocol;
1415
using Microsoft.Extensions.DependencyInjection.Extensions;
1516
using Microsoft.Extensions.Options;
@@ -65,6 +66,7 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
6566
services.TryAddSingleton<ComponentParameterDeserializer>();
6667
services.TryAddSingleton<ComponentParametersTypeCache>();
6768
services.TryAddSingleton<CircuitIdFactory>();
69+
services.TryAddScoped<IErrorBoundaryLogger, RemoteErrorBoundaryLogger>();
6870

6971
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
7072
services.TryAddScoped<ICircuitAccessor, DefaultCircuitAccessor>();

src/Components/Web/src/PublicAPI.Unshipped.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ Microsoft.AspNetCore.Components.Forms.InputTextArea.Element.get -> Microsoft.Asp
1515
Microsoft.AspNetCore.Components.Forms.InputTextArea.Element.set -> void
1616
*REMOVED*static Microsoft.AspNetCore.Components.Forms.BrowserFileExtensions.RequestImageFileAsync(this Microsoft.AspNetCore.Components.Forms.IBrowserFile! browserFile, string! format, int maxWith, int maxHeight) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Components.Forms.IBrowserFile!>
1717
Microsoft.AspNetCore.Components.Web.ErrorBoundary
18+
Microsoft.AspNetCore.Components.Web.ErrorBoundary.AutoReset.get -> bool
19+
Microsoft.AspNetCore.Components.Web.ErrorBoundary.AutoReset.set -> void
1820
Microsoft.AspNetCore.Components.Web.ErrorBoundary.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment?
1921
Microsoft.AspNetCore.Components.Web.ErrorBoundary.ChildContent.set -> void
2022
Microsoft.AspNetCore.Components.Web.ErrorBoundary.ErrorBoundary() -> void
21-
Microsoft.AspNetCore.Components.Web.ErrorBoundary.ErrorContent.get -> Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.Web.ErrorBoundary.FormattedError!>?
23+
Microsoft.AspNetCore.Components.Web.ErrorBoundary.ErrorContent.get -> Microsoft.AspNetCore.Components.RenderFragment<System.Exception!>?
2224
Microsoft.AspNetCore.Components.Web.ErrorBoundary.ErrorContent.set -> void
23-
Microsoft.AspNetCore.Components.Web.ErrorBoundary.FormattedError
24-
Microsoft.AspNetCore.Components.Web.ErrorBoundary.FormattedError.Details.get -> string!
25-
Microsoft.AspNetCore.Components.Web.ErrorBoundary.FormattedError.FormattedError(string! message, string! details) -> void
26-
Microsoft.AspNetCore.Components.Web.ErrorBoundary.FormattedError.Message.get -> string!
2725
Microsoft.AspNetCore.Components.Web.ErrorBoundary.HandleException(System.Exception! exception) -> void
26+
Microsoft.AspNetCore.Components.Web.IErrorBoundaryLogger
27+
Microsoft.AspNetCore.Components.Web.IErrorBoundaryLogger.LogErrorAsync(System.Exception! exception, bool clientOnly) -> System.Threading.Tasks.ValueTask
2828
static Microsoft.AspNetCore.Components.ElementReferenceExtensions.FocusAsync(this Microsoft.AspNetCore.Components.ElementReference elementReference, bool preventScroll) -> System.Threading.Tasks.ValueTask
2929
static Microsoft.AspNetCore.Components.Forms.BrowserFileExtensions.RequestImageFileAsync(this Microsoft.AspNetCore.Components.Forms.IBrowserFile! browserFile, string! format, int maxWidth, int maxHeight) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Components.Forms.IBrowserFile!>
3030
Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor.EventName.get -> string!

src/Components/Web/src/Web/ErrorBoundary.cs

Lines changed: 22 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,32 @@
44
using System;
55
using System.Threading.Tasks;
66
using Microsoft.AspNetCore.Components.Rendering;
7-
using Microsoft.JSInterop;
87

98
namespace Microsoft.AspNetCore.Components.Web
109
{
1110
// TODO: Reimplement directly on IComponent
1211
public sealed class ErrorBoundary : ComponentBase, IErrorBoundary
1312
{
14-
private FormattedError? _currentError;
13+
private Exception? _currentException;
1514

16-
[Inject] private IJSRuntime? JS { get; set; }
15+
[Inject] private IErrorBoundaryLogger? ErrorBoundaryLogger { get; set; }
16+
17+
/// <summary>
18+
/// Specifies whether to reset the error state each time the <see cref="ErrorBoundary"/> is rendered
19+
/// by its parent. This allows the child content to be recreated in an attempt to recover from the error.
20+
/// </summary>
21+
[Parameter] public bool AutoReset { get; set; }
1722

1823
[Parameter] public RenderFragment? ChildContent { get; set; }
1924

20-
// Notice that, to respect the IClientErrorFormatter, we have to do this in terms of a Message/Details
21-
// pair, and not in terms of the original Exception. We can't assume that developers who provide an
22-
// ErrorContent understand the issues with exposing the raw exception to the client.
23-
[Parameter] public RenderFragment<FormattedError>? ErrorContent { get; set; }
25+
[Parameter] public RenderFragment<Exception>? ErrorContent { get; set; }
2426

25-
// TODO: Eliminate the enableDetailedErrors flag (and corresponding API on Renderer)
26-
// and instead have some new default DI service, IClientErrorFormatter.
2727
public void HandleException(Exception exception)
2828
{
29-
// TODO: We should log the underlying exception to some ILogger using the same
30-
// severity that we did before for unhandled exceptions
31-
_currentError = FormatErrorForClient(exception);
32-
StateHasChanged();
29+
_ = ErrorBoundaryLogger!.LogErrorAsync(exception, clientOnly: false);
3330

34-
// TODO: Should there be an option not to auto-log exceptions to the console?
35-
// Probably not. Don't want people thinking it's fine to have such exceptions.
36-
_ = LogExceptionToClientIfPossible();
31+
_currentException = exception;
32+
StateHasChanged();
3733
}
3834

3935
protected override void OnParametersSet()
@@ -42,19 +38,23 @@ protected override void OnParametersSet()
4238
// re-renders us. This has the benefit that in your layout, you can just wrap this
4339
// around @Body and it does what you expect (recovering on navigate). But it might
4440
// make other cases more awkward because the parent will keep recreating any children
45-
// that just error out on init. Maybe it should be an option.
46-
_currentError = null;
41+
// that just error out on init.
42+
if (AutoReset)
43+
{
44+
_currentException = null;
45+
}
4746
}
4847

4948
protected override void BuildRenderTree(RenderTreeBuilder builder)
5049
{
51-
if (_currentError is null)
50+
var exception = _currentException;
51+
if (exception is null)
5252
{
5353
builder.AddContent(0, ChildContent);
5454
}
5555
else if (ErrorContent is not null)
5656
{
57-
builder.AddContent(1, ErrorContent(_currentError));
57+
builder.AddContent(1, ErrorContent(exception));
5858
}
5959
else
6060
{
@@ -66,46 +66,12 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
6666
// rely on there being any CSS, but make it overridable via CSS.
6767
builder.OpenElement(0, "div");
6868
builder.AddAttribute(1, "onclick", (Func<MouseEventArgs, ValueTask>)(_ =>
69-
{
70-
return LogExceptionToClientIfPossible();
71-
}));
72-
69+
// Re-log, to help the developer figure out which ErrorBoundary issued which log message
70+
ErrorBoundaryLogger!.LogErrorAsync(exception, clientOnly: true)));
7371
builder.AddContent(1, "Error");
7472
builder.CloseElement();
7573
builder.CloseRegion();
7674
}
7775
}
78-
79-
private async ValueTask LogExceptionToClientIfPossible()
80-
{
81-
// TODO: Handle prerendering too. We can't use IJSRuntime while prerendering.
82-
if (_currentError is not null)
83-
{
84-
await JS!.InvokeVoidAsync("console.error", $"{_currentError.Message}\n{_currentError.Details}");
85-
}
86-
}
87-
88-
// TODO: Move into a new IClientErrorFormatter service
89-
private static FormattedError FormatErrorForClient(Exception exception)
90-
{
91-
// TODO: Obviously this will be internal to IClientErrorFormatter
92-
var enableDetailedErrors = true;
93-
94-
return enableDetailedErrors
95-
? new FormattedError(exception.Message, exception.StackTrace ?? string.Empty)
96-
: new FormattedError("There was an error", "For more details turn on detailed exceptions by setting 'DetailedErrors: true' in 'appSettings.Development.json' or set 'CircuitOptions.DetailedErrors'.");
97-
}
98-
99-
public record FormattedError
100-
{
101-
public FormattedError(string message, string details)
102-
{
103-
Message = message;
104-
Details = details;
105-
}
106-
107-
public string Message { get; }
108-
public string Details { get; }
109-
}
11076
}
11177
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.AspNetCore.Components.Web
8+
{
9+
// The rules arround logging differ between environments.
10+
// - For WebAssembly, we always log to the console with detailed information
11+
// - For Server, we log to both the server-side log (with detailed information), and to the
12+
// client (respecting the DetailedError option)
13+
// - In prerendering, we log only to the server-side log
14+
15+
/// <summary>
16+
/// Logs exception information for a <see cref="ErrorBoundary"/> component.
17+
/// </summary>
18+
public interface IErrorBoundaryLogger
19+
{
20+
/// <summary>
21+
/// Logs the supplied <paramref name="exception"/>.
22+
/// </summary>
23+
/// <param name="exception">The <see cref="Exception"/> to log.</param>
24+
/// <param name="clientOnly">If true, indicates that the error should only be logged to the client (e.g., because it was already logged to the server).</param>
25+
/// <returns>A <see cref="ValueTask"/> representing the completion of the operation.</returns>
26+
ValueTask LogErrorAsync(Exception exception, bool clientOnly);
27+
}
28+
}

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ internal void InitializeDefaultServices()
246246
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
247247
Services.AddSingleton<ComponentApplicationLifetime>();
248248
Services.AddSingleton<ComponentApplicationState>(sp => sp.GetRequiredService<ComponentApplicationLifetime>().State);
249+
Services.AddSingleton<IErrorBoundaryLogger, WebAssemblyErrorBoundaryLogger>();
249250
Services.AddLogging(builder =>
250251
{
251252
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Components.Web;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
10+
{
11+
internal class WebAssemblyErrorBoundaryLogger : IErrorBoundaryLogger
12+
{
13+
private readonly ILogger<ErrorBoundary> _errorBoundaryLogger;
14+
15+
public WebAssemblyErrorBoundaryLogger(ILogger<ErrorBoundary> errorBoundaryLogger)
16+
{
17+
_errorBoundaryLogger = errorBoundaryLogger ?? throw new ArgumentNullException(nameof(errorBoundaryLogger)); ;
18+
}
19+
20+
public ValueTask LogErrorAsync(Exception exception, bool clientOnly)
21+
{
22+
// For, client-side code, all internal state is visible to the end user. We can just
23+
// log directly to the console.
24+
_errorBoundaryLogger.LogError(exception.ToString());
25+
return ValueTask.CompletedTask;
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)