diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index 996cd4703ef9..efc144de55e9 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; +using System.Diagnostics; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder @@ -26,7 +28,7 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a throw new ArgumentNullException(nameof(app)); } - return app.UseMiddleware(); + return SetExceptionHandlerMiddleware(app, options: null); } /// @@ -95,7 +97,50 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a throw new ArgumentNullException(nameof(options)); } - return app.UseMiddleware(Options.Create(options)); + var iOptions = Options.Create(options); + return SetExceptionHandlerMiddleware(app, iOptions); + } + + private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBuilder app, IOptions? options) + { + const string globalRouteBuilderKey = "__GlobalEndpointRouteBuilder"; + // Only use this path if there's a global router (in the 'WebApplication' case). + if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) + { + return app.Use(next => + { + var loggerFactory = app.ApplicationServices.GetRequiredService(); + var diagnosticListener = app.ApplicationServices.GetRequiredService(); + + if (options is null) + { + options = app.ApplicationServices.GetRequiredService>(); + } + + if (!string.IsNullOrEmpty(options.Value.ExceptionHandlingPath) && options.Value.ExceptionHandler is null) + { + // start a new middleware pipeline + var builder = app.New(); + // use the old routing pipeline if it exists so we preserve all the routes and matching logic + // ((IApplicationBuilder)WebApplication).New() does not copy globalRouteBuilderKey automatically like it does for all other properties. + builder.Properties[globalRouteBuilderKey] = routeBuilder; + builder.UseRouting(); + // apply the next middleware + builder.Run(next); + // store the pipeline for the error case + options.Value.ExceptionHandler = builder.Build(); + } + + return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener).Invoke; + }); + } + + if (options is null) + { + return app.UseMiddleware(); + } + + return app.UseMiddleware(options); } } } diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs index 960574f04bcc..06c4a349c053 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs @@ -174,7 +174,34 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute( throw new ArgumentNullException(nameof(app)); } - return app.UseStatusCodePages(async context => + const string globalRouteBuilderKey = "__GlobalEndpointRouteBuilder"; + // Only use this path if there's a global router (in the 'WebApplication' case). + if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) + { + return app.Use(next => + { + RequestDelegate? newNext = null; + // start a new middleware pipeline + var builder = app.New(); + // use the old routing pipeline if it exists so we preserve all the routes and matching logic + // ((IApplicationBuilder)WebApplication).New() does not copy globalRouteBuilderKey automatically like it does for all other properties. + builder.Properties[globalRouteBuilderKey] = routeBuilder; + builder.UseRouting(); + // apply the next middleware + builder.Run(next); + newNext = builder.Build(); + + return new StatusCodePagesMiddleware(next, + Options.Create(new StatusCodePagesOptions() { HandleAsync = CreateHandler(pathFormat, queryFormat, newNext) })).Invoke; + }); + } + + return app.UseStatusCodePages(CreateHandler(pathFormat, queryFormat)); + } + + private static Func CreateHandler(string pathFormat, string? queryFormat, RequestDelegate? next = null) + { + var handler = async (StatusCodeContext context) => { var newPath = new PathString( string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode)); @@ -202,7 +229,14 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute( context.HttpContext.Request.QueryString = newQueryString; try { - await context.Next(context.HttpContext); + if (next is not null) + { + await next(context.HttpContext); + } + else + { + await context.Next(context.HttpContext); + } } finally { @@ -210,7 +244,9 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute( context.HttpContext.Request.Path = originalPath; context.HttpContext.Features.Set(null); } - }); + }; + + return handler; } } } diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs index 3e65cf7a4a39..75f49e4b4b04 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Diagnostics @@ -657,5 +658,258 @@ public async Task ExceptionHandler_CanReturn404Responses_WhenAllowed() && w.EventId == 4 && w.Message == "No exception handler was found, rethrowing original exception."); } + + [Fact] + public async Task ExceptionHandlerWithOwnBuilder() + { + var sink = new TestSink(TestSink.EnableWithTypeName); + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + app.UseExceptionHandler(builder => + { + builder.Run(c => + { + c.Response.StatusCode = 200; + return c.Response.WriteAsync("separate pipeline"); + }); + }); + + app.Map("/throw", (innerAppBuilder) => + { + innerAppBuilder.Run(httpContext => + { + throw new InvalidOperationException("Something bad happened."); + }); + }); + }); + }).Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var response = await client.GetAsync("throw"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("separate pipeline", await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task ExceptionHandlerWithPathWorksAfterUseRoutingIfGlobalRouteBuilderUsed() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.Use(async (httpContext, next) => + { + Exception exception = null; + try + { + await next(httpContext); + } + catch (InvalidOperationException ex) + { + exception = ex; + } + + Assert.Null(exception); + }); + + app.UseRouting(); + + app.UseExceptionHandler("/handle-errors"); + + app.UseEndpoints(endpoints => + { + endpoints.Map("/handle-errors", c => { + c.Response.StatusCode = 200; + return c.Response.WriteAsync("Handled"); + }); + }); + + app.Run((httpContext) => + { + throw new InvalidOperationException("Something bad happened"); + }); + + await app.StartAsync(); + + using (var server = app.GetTestServer()) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + Assert.Equal("Handled", await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task ExceptionHandlerWithOptionsWorksAfterUseRoutingIfGlobalRouteBuilderUsed() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.Use(async (httpContext, next) => + { + Exception exception = null; + try + { + await next(httpContext); + } + catch (InvalidOperationException ex) + { + exception = ex; + } + + Assert.Null(exception); + }); + + app.UseRouting(); + + app.UseExceptionHandler(new ExceptionHandlerOptions() + { + ExceptionHandlingPath = "/handle-errors" + }); + + app.UseEndpoints(endpoints => + { + endpoints.Map("/handle-errors", c => { + c.Response.StatusCode = 200; + return c.Response.WriteAsync("Handled"); + }); + }); + + app.Run((httpContext) => + { + throw new InvalidOperationException("Something bad happened"); + }); + + await app.StartAsync(); + + using (var server = app.GetTestServer()) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + Assert.Equal("Handled", await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task ExceptionHandlerWithAddWorksAfterUseRoutingIfGlobalRouteBuilderUsed() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddExceptionHandler(o => o.ExceptionHandlingPath = "/handle-errors"); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.Use(async (httpContext, next) => + { + Exception exception = null; + try + { + await next(httpContext); + } + catch (InvalidOperationException ex) + { + exception = ex; + } + + Assert.Null(exception); + }); + + app.UseRouting(); + + app.UseExceptionHandler(); + + app.UseEndpoints(endpoints => + { + endpoints.Map("/handle-errors", c => { + c.Response.StatusCode = 200; + return c.Response.WriteAsync("Handled"); + }); + }); + + app.Run((httpContext) => + { + throw new InvalidOperationException("Something bad happened"); + }); + + await app.StartAsync(); + + using (var server = app.GetTestServer()) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + Assert.Equal("Handled", await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task ExceptionHandlerWithExceptionHandlerNotReplacedWithGlobalRouteBuilder() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.Use(async (httpContext, next) => + { + Exception exception = null; + try + { + await next(httpContext); + } + catch (InvalidOperationException ex) + { + exception = ex; + } + + Assert.Null(exception); + }); + + app.UseRouting(); + + app.UseExceptionHandler(new ExceptionHandlerOptions() + { + ExceptionHandler = httpContext => + { + httpContext.Response.StatusCode = StatusCodes.Status404NotFound; + return httpContext.Response.WriteAsync("Custom handler"); + } + }); + + app.UseEndpoints(endpoints => + { + endpoints.Map("/handle-errors", c => { + c.Response.StatusCode = 200; + return c.Response.WriteAsync("Handled"); + }); + }); + + app.Run((httpContext) => + { + throw new InvalidOperationException("Something bad happened"); + }); + + await app.StartAsync(); + + using (var server = app.GetTestServer()) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Equal("Custom handler", await response.Content.ReadAsStringAsync()); + } + } } } diff --git a/src/Middleware/Diagnostics/test/UnitTests/Microsoft.AspNetCore.Diagnostics.Tests.csproj b/src/Middleware/Diagnostics/test/UnitTests/Microsoft.AspNetCore.Diagnostics.Tests.csproj index c1b33b09a299..8a7999f7eeb0 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/Microsoft.AspNetCore.Diagnostics.Tests.csproj +++ b/src/Middleware/Diagnostics/test/UnitTests/Microsoft.AspNetCore.Diagnostics.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs index 1dc5e4912914..cc853db20408 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Diagnostics @@ -193,5 +194,41 @@ await httpContext.Response.WriteAsync( var content = await response.Content.ReadAsStringAsync(); Assert.Equal($"?id={expectedStatusCode}, /location, ?name=James", content); } + + [Fact] + public async Task Reexecute_WorksAfterUseRoutingWithGlobalRouteBuilder() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.UseRouting(); + + app.UseStatusCodePagesWithReExecute(pathFormat: "/errorPage", queryFormat: "?id={0}"); + + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", c => + { + c.Response.StatusCode = 404; + return Task.CompletedTask; + }); + + endpoints.MapGet("/errorPage", () => "errorPage"); + }); + + app.Run((context) => + { + throw new InvalidOperationException("Invalid input provided."); + }); + + await app.StartAsync(); + + using var server = app.GetTestServer(); + var client = server.CreateClient(); + var response = await client.GetAsync("/"); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("errorPage", content); + } } } diff --git a/src/Middleware/Rewrite/src/Microsoft.AspNetCore.Rewrite.csproj b/src/Middleware/Rewrite/src/Microsoft.AspNetCore.Rewrite.csproj index 7ea6d0556c2c..f2d2f2233038 100644 --- a/src/Middleware/Rewrite/src/Microsoft.AspNetCore.Rewrite.csproj +++ b/src/Middleware/Rewrite/src/Microsoft.AspNetCore.Rewrite.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Middleware/Rewrite/src/RewriteBuilderExtensions.cs b/src/Middleware/Rewrite/src/RewriteBuilderExtensions.cs index 874fc0f68531..d7149ffcc8bd 100644 --- a/src/Middleware/Rewrite/src/RewriteBuilderExtensions.cs +++ b/src/Middleware/Rewrite/src/RewriteBuilderExtensions.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder @@ -24,7 +26,7 @@ public static IApplicationBuilder UseRewriter(this IApplicationBuilder app) throw new ArgumentNullException(nameof(app)); } - return app.UseMiddleware(); + return AddRewriteMiddleware(app, options: null); } /// @@ -46,7 +48,45 @@ public static IApplicationBuilder UseRewriter(this IApplicationBuilder app, Rewr } // put middleware in pipeline - return app.UseMiddleware(Options.Create(options)); + return AddRewriteMiddleware(app, Options.Create(options)); + } + + private static IApplicationBuilder AddRewriteMiddleware(IApplicationBuilder app, IOptions? options) + { + const string globalRouteBuilderKey = "__GlobalEndpointRouteBuilder"; + // Only use this path if there's a global router (in the 'WebApplication' case). + if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) + { + return app.Use(next => + { + if (options is null) + { + options = app.ApplicationServices.GetRequiredService>(); + } + + var webHostEnv = app.ApplicationServices.GetRequiredService(); + var loggerFactory = app.ApplicationServices.GetRequiredService(); + + // start a new middleware pipeline + var builder = app.New(); + // use the old routing pipeline if it exists so we preserve all the routes and matching logic + // ((IApplicationBuilder)WebApplication).New() does not copy globalRouteBuilderKey automatically like it does for all other properties. + builder.Properties[globalRouteBuilderKey] = routeBuilder; + builder.UseRouting(); + // apply the next middleware + builder.Run(next); + options.Value.BranchedNext = builder.Build(); + + return new RewriteMiddleware(next, webHostEnv, loggerFactory, options).Invoke; + }); + } + + if (options is null) + { + return app.UseMiddleware(); + } + + return app.UseMiddleware(options); } } } diff --git a/src/Middleware/Rewrite/src/RewriteMiddleware.cs b/src/Middleware/Rewrite/src/RewriteMiddleware.cs index bfb28fc0fe06..1600788df06d 100644 --- a/src/Middleware/Rewrite/src/RewriteMiddleware.cs +++ b/src/Middleware/Rewrite/src/RewriteMiddleware.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Rewrite.Logging; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; @@ -73,6 +74,8 @@ public Task Invoke(HttpContext context) Result = RuleResult.ContinueRules }; + var originalPath = context.Request.Path; + foreach (var rule in _options.Rules) { rule.ApplyRule(rewriteContext); @@ -93,6 +96,24 @@ public Task Invoke(HttpContext context) throw new ArgumentOutOfRangeException($"Invalid rule termination {rewriteContext.Result}"); } } + + // If a rule changed the path we want routing to find a new endpoint + if (originalPath != context.Request.Path) + { + if (_options.BranchedNext is not null) + { + // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset + // the endpoint and route values to ensure things are re-calculated. + context.SetEndpoint(endpoint: null); + var routeValuesFeature = context.Features.Get(); + if (routeValuesFeature is not null) + { + routeValuesFeature.RouteValues = null!; + } + return _options.BranchedNext(context); + } + } + return _next(context); } } diff --git a/src/Middleware/Rewrite/src/RewriteOptions.cs b/src/Middleware/Rewrite/src/RewriteOptions.cs index 55a043948003..b1fb54fbc00b 100644 --- a/src/Middleware/Rewrite/src/RewriteOptions.cs +++ b/src/Middleware/Rewrite/src/RewriteOptions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.Extensions.FileProviders; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Rewrite { @@ -24,5 +25,7 @@ public class RewriteOptions /// Defaults to . /// public IFileProvider StaticFileProvider { get; set; } = default!; + + internal RequestDelegate? BranchedNext { get; set; } } } diff --git a/src/Middleware/Rewrite/test/Microsoft.AspNetCore.Rewrite.Tests.csproj b/src/Middleware/Rewrite/test/Microsoft.AspNetCore.Rewrite.Tests.csproj index da3f5187898c..f62c4140b7f1 100644 --- a/src/Middleware/Rewrite/test/Microsoft.AspNetCore.Rewrite.Tests.csproj +++ b/src/Middleware/Rewrite/test/Microsoft.AspNetCore.Rewrite.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Middleware/Rewrite/test/MiddlewareTests.cs b/src/Middleware/Rewrite/test/MiddlewareTests.cs index cd38a0a00963..d27715620ef7 100644 --- a/src/Middleware/Rewrite/test/MiddlewareTests.cs +++ b/src/Middleware/Rewrite/test/MiddlewareTests.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Rewrite.Tests.CodeRules @@ -513,6 +514,43 @@ public async Task CheckIfEmptyStringRedirectCorrectly() Assert.Equal("/", response.Headers.Location.OriginalString); } + [Fact] + public async Task RewriteAfterUseRoutingHitsOriginalEndpoint() + { + // This is an edge case where users setup routing incorrectly, but we don't want to accidentally change behavior in case someone + // relies on it, so we have this test + var options = new RewriteOptions().AddRewrite("(.*)", "$1s", skipRemainingRules: false); + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(s => + { + s.AddRouting(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseRewriter(options); + + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/foos", context => context.Response.WriteAsync("bad")); + endpoints.MapGet("/foo", context => context.Response.WriteAsync($"{context.GetEndpoint()?.DisplayName} from {context.Request.Path}")); + }); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + var response = await server.CreateClient().GetStringAsync("foo"); + + Assert.Equal("/foo HTTP: GET from /foos", response); + } + [Fact] public async Task CheckIfEmptyStringRewriteCorrectly() { @@ -680,5 +718,78 @@ public async Task CheckRedirectToWwwWithStatusCodeInWhitelistedDomains(int statu Assert.Equal(statusCode, (int)response.StatusCode); } + [Theory] + [InlineData("(.*)", "http://example.com/g")] + [InlineData("/", "no rule")] + public async Task Rewrite_WorksAfterUseRoutingIfGlobalRouteBuilderUsed(string regex, string output) + { + var options = new RewriteOptions().AddRewrite(regex, "http://example.com/g", skipRemainingRules: false); + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.UseRouting(); + + app.UseRewriter(options); + + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/foo", context => context.Response.WriteAsync( + "no rule")); + + endpoints.MapGet("/g", context => context.Response.WriteAsync( + context.Request.Scheme + + "://" + + context.Request.Host + + context.Request.Path + + context.Request.QueryString)); + }); + + await app.StartAsync(); + + var server = app.GetTestServer(); + + var response = await server.CreateClient().GetStringAsync("foo"); + + Assert.Equal(output, response); + } + + [Theory] + [InlineData("(.*)", "http://example.com/g")] + [InlineData("/", "no rule")] + public async Task RewriteFromOptions_WorksAfterUseRoutingIfGlobalRouteBuilderUsed(string regex, string output) + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + builder.Services.Configure(options => + { + options.AddRewrite(regex, "http://example.com/g", skipRemainingRules: false); + }); + await using var app = builder.Build(); + app.UseRouting(); + + app.UseRewriter(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/foo", context => context.Response.WriteAsync( + "no rule")); + + endpoints.MapGet("/g", context => context.Response.WriteAsync( + context.Request.Scheme + + "://" + + context.Request.Host + + context.Request.Path + + context.Request.QueryString)); + }); + + await app.StartAsync(); + + var server = app.GetTestServer(); + + var response = await server.CreateClient().GetStringAsync("foo"); + + Assert.Equal(output, response); + } } } diff --git a/src/submodules/googletest b/src/submodules/googletest index 0134d73a4902..2f80c2ba71c0 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit 0134d73a4902574269ff2e42827f7573d3df08ae +Subproject commit 2f80c2ba71c0e8922a03b9b855e5b019ad1f7064 diff --git a/src/submodules/spa-templates b/src/submodules/spa-templates index 373712a22074..dc078f5b8efa 160000 --- a/src/submodules/spa-templates +++ b/src/submodules/spa-templates @@ -1 +1 @@ -Subproject commit 373712a22074c9cec3461dc5322300ccfc229ef7 +Subproject commit dc078f5b8efa3a2e6d92c40c8838326f7a1da940