Skip to content

Commit 7780b9e

Browse files
committed
Throw unhandled exceptions during prerendering
Fixes: #8609 Currently exceptions thrown during prerendering are simply logged. This change uses the existing *unhandled exception* mechanism of the renderer/circuit to throw these. The result is that the developer exception page just works for prerendering.
1 parent cb146ab commit 7780b9e

File tree

5 files changed

+60
-17
lines changed

5 files changed

+60
-17
lines changed

src/Components/Server/src/Circuits/CircuitPrerenderer.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using System.Collections.Generic;
6+
using System.Runtime.ExceptionServices;
57
using System.Threading.Tasks;
68
using Microsoft.AspNetCore.Http;
79
using Microsoft.AspNetCore.Http.Extensions;
@@ -26,8 +28,12 @@ public async Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerende
2628
GetFullUri(context.Request),
2729
GetFullBaseUri(context.Request));
2830

31+
// We don't need to unsubscribe because the circuit host object is scoped to this call.
32+
circuitHost.UnhandledException += CircuitHost_UnhandledException;
33+
2934
// For right now we just do prerendering and dispose the circuit. In the future we will keep the circuit around and
30-
// reconnect to it from the ComponentsHub.
35+
// reconnect to it from the ComponentsHub. If we keep the circuit/renderer we also need to unsubscribe this error
36+
// handler.
3137
try
3238
{
3339
return await circuitHost.PrerenderComponentAsync(
@@ -40,6 +46,13 @@ public async Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerende
4046
}
4147
}
4248

49+
private void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e)
50+
{
51+
// Throw all exceptions encountered during pre-rendering so the default developer
52+
// error page can respond.
53+
ExceptionDispatchInfo.Capture((Exception)e.ExceptionObject).Throw();
54+
}
55+
4356
private string GetFullUri(HttpRequest request)
4457
{
4558
return UriHelper.BuildAbsolute(

src/Components/Server/src/Circuits/RemoteRenderer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ protected override void HandleException(Exception exception)
9494
{
9595
Log.UnhandledExceptionRenderingComponent(_logger, exception);
9696
}
97+
98+
UnhandledException?.Invoke(this, exception);
9799
}
98100

99101
/// <inheritdoc />

src/Mvc/test/Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using System.Net;
56
using System.Net.Http;
67
using System.Threading.Tasks;
78
using AngleSharp.Parser.Html;
89
using BasicWebSite;
910
using BasicWebSite.Services;
11+
using Microsoft.AspNetCore.Hosting;
1012
using Microsoft.Extensions.DependencyInjection;
1113
using Xunit;
1214

@@ -17,18 +19,17 @@ public class ComponentRenderingFunctionalTests : IClassFixture<MvcTestFixture<Ba
1719
public ComponentRenderingFunctionalTests(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
1820
{
1921
Factory = fixture;
20-
Client = Client ?? CreateClient(fixture);
2122
}
2223

23-
public HttpClient Client { get; }
24-
2524
public MvcTestFixture<StartupWithoutEndpointRouting> Factory { get; }
2625

2726
[Fact]
2827
public async Task Renders_BasicComponent()
2928
{
3029
// Arrange & Act
31-
var response = await Client.GetAsync("http://localhost/components");
30+
var client = CreateClient(Factory);
31+
32+
var response = await client.GetAsync("http://localhost/components");
3233

3334
// Assert
3435
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@@ -41,9 +42,7 @@ public async Task Renders_BasicComponent()
4142
public async Task Renders_BasicComponent_UsingRazorComponents_Prerrenderer()
4243
{
4344
// Arrange & Act
44-
var client = Factory
45-
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents()))
46-
.CreateClient();
45+
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
4746

4847
var response = await client.GetAsync("http://localhost/components");
4948

@@ -58,7 +57,9 @@ public async Task Renders_BasicComponent_UsingRazorComponents_Prerrenderer()
5857
public async Task Renders_RoutingComponent()
5958
{
6059
// Arrange & Act
61-
var response = await Client.GetAsync("http://localhost/components/routable");
60+
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
61+
62+
var response = await client.GetAsync("http://localhost/components/routable");
6263

6364
// Assert
6465
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@@ -71,9 +72,7 @@ public async Task Renders_RoutingComponent()
7172
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerrenderer()
7273
{
7374
// Arrange & Act
74-
var client = Factory
75-
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents()))
76-
.CreateClient();
75+
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
7776

7877
var response = await client.GetAsync("http://localhost/components/routable");
7978

@@ -84,6 +83,21 @@ public async Task Renders_RoutingComponent_UsingRazorComponents_Prerrenderer()
8483
AssertComponent("\n Router component\n<p>Routed successfully</p>\n", "Routing", content);
8584
}
8685

86+
[Fact]
87+
public async Task Renders_ThrowingComponent_UsingRazorComponents_Prerrenderer()
88+
{
89+
// Arrange & Act
90+
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
91+
92+
var response = await client.GetAsync("http://localhost/components/throws");
93+
94+
// Assert
95+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
96+
var content = await response.Content.ReadAsStringAsync();
97+
98+
Assert.Contains("InvalidTimeZoneException: test", content);
99+
}
100+
87101
[Fact]
88102
public async Task Renders_AsyncComponent()
89103
{
@@ -138,8 +152,8 @@ public async Task Renders_AsyncComponent()
138152
</table>
139153
140154
";
141-
142-
var response = await Client.GetAsync("http://localhost/components");
155+
var client = CreateClient(Factory);
156+
var response = await client.GetAsync("http://localhost/components");
143157

144158
// Assert
145159
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@@ -164,12 +178,16 @@ private class LoopHttpHandler : DelegatingHandler
164178
{
165179
}
166180

167-
private HttpClient CreateClient(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
181+
private HttpClient CreateClient(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture, Action<IWebHostBuilder> configure = null)
168182
{
169183
var loopHandler = new LoopHttpHandler();
170184

171185
var client = fixture
172-
.WithWebHostBuilder(builder => builder.ConfigureServices(ConfigureTestWeatherForecastService))
186+
.WithWebHostBuilder(builder =>
187+
{
188+
configure?.Invoke(builder);
189+
builder.ConfigureServices(ConfigureTestWeatherForecastService);
190+
})
173191
.CreateClient();
174192

175193
// We configure the inner handler with a handler to this TestServer instance so that calls to the

src/Mvc/test/WebSites/BasicWebSite/Controllers/RazorComponentsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public class RazorComponentsController : Controller
5151
};
5252

5353
[HttpGet("/components")]
54-
[HttpGet("/components/routable")]
54+
[HttpGet("/components/{component}")]
5555
public IActionResult Index()
5656
{
5757
return View();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@page "/components/throws"
2+
3+
@* This is expected to throw and result in a 500 *@
4+
@functions {
5+
protected override async Task OnInitAsync()
6+
{
7+
await base.OnInitAsync();
8+
throw new InvalidTimeZoneException("test");
9+
}
10+
}

0 commit comments

Comments
 (0)