I'm in the process of developing a web app using an ASP.NET Core Web API (in a Docker container) and a separate Blazor web app. I've added individual user account functionality for authorization/authentication using Jwt Bearer tokens. Using the API's auth endpoints in Swagger works as expected. However, when trying to call the API with POST
or PUT
requests in the Blazor web app, an error is thrown by both the API and the app. GET
requests are working correctly and return data.
The Login POST
response is generating an OK response from the API with the Json data correctly serialized. The exception triggers when running each middleware after the response.
A significant amount of code used for the auth controller was taken from the Visual Studio templates, so I would expect it to work as-is.
Here's a snippet of code from the auth controller (truncated for brevity):
[HttpPost]
public async Task<Results<Ok<LoginResponseDTO>, ProblemHttpResult>>
Login([FromBody] LoginRequestDTO loginRequestDto)
{
...
var loginResult = await signInManager.PasswordSignInAsync(...);
...
var token = tokenRepository.GetAccessTokenResponse(user, principal, [.. roles]);
logger.LogInformation(message: $"UserController.Login: Login success: {user.Id} - {loginResult}");
return TypedResults.Ok(token); // ERROR IS THROWN HERE, DURING MIDDLEWARE EXECUTION
}
The API's Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("Logs/SocialMediaAppAPI.txt", rollingInterval: RollingInterval.Day)
.MinimumLevel.Debug()
.CreateLogger();
builder.Logging.ClearProviders();
builder.Logging.AddSerilog(logger);
builder.Services.AddControllers();
builder.Services.Configure<RouteOptions>(options =>
{
options.LowercaseUrls = true;
});
builder.Services.AddHttpContextAccessor();
builder.Services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(0, 1);
options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme
{
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = JwtBearerDefaults.AuthenticationScheme
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = JwtBearerDefaults.AuthenticationScheme
},
Scheme = "Oauth2",
Name = JwtBearerDefaults.AuthenticationScheme,
In = ParameterLocation.Header
},
new List<string>()
}
});
});
builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();
builder.Services.AddOptions<AuthOptions>()
.Bind(builder.Configuration.GetRequiredSection(AuthOptions.AuthTitle))
.ValidateDataAnnotations()
.ValidateOnStart();
var contentDbConnectionString = builder.Configuration.GetConnectionString("ContentDbConnectionString")
?? throw new InvalidOperationException("Connection string 'ContentDbConnectionString' not found.");
var authDbConnectionString = builder.Configuration.GetConnectionString("AuthDbConnectionString")
?? throw new InvalidOperationException("Connection string 'AuthDbConnectionString' not found.");
builder.Services.AddDbContext<ContentDbContext>(options =>
options.UseSqlServer(contentDbConnectionString)
);
builder.Services.AddDbContext<AuthDbContext>(options =>
options.UseSqlServer(authDbConnectionString)
);
builder.Services.AddAutoMapper(typeof(AutoMapperProfiles));
builder.Services.AddScoped<IEmailSender<ApplicationUser>, AuthNoOpEmailSender>();
builder.Services.AddScoped<IProfileRepository, SqlProfileRepository>();
builder.Services.AddScoped<IPostRepository, SqlPostRepository>();
builder.Services.AddScoped<ITokenRepository, TokenRepository>();
builder.Services.AddScoped<IMediaRepository, LocalMediaRepository>();
builder.Services.AddIdentityCore<ApplicationUser>()
.AddRoles<ApplicationRole>()
.AddTokenProvider<DataProtectorTokenProvider<ApplicationUser>>("SocialMediaAppAPI")
.AddEntityFrameworkStores<AuthDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
builder.Services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 8;
options.Password.RequiredUniqueChars = 1;
options.User.RequireUniqueEmail = true;
//options.Stores.ProtectPersonalData = true;
});
var jwtKey = builder.Configuration["Jwt:Key"]
?? throw new InvalidOperationException("Configuration value 'Jwt:Key' not found.");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
})
.AddBearerToken(IdentityConstants.BearerScheme)
.AddIdentityCookies();
builder.Services.AddAuthorizationBuilder()
.AddPolicy(AuthOptions.AdminUserPolicyName, policy =>
{
policy.RequireClaim(builder.Configuration["Auth:DefaultAdminRole"]);
});
var app = builder.Build();
var versionDescriptions = app.DescribeApiVersions();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
foreach(var description in versionDescriptions)
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
});
}
//app.UseMiddleware<ExceptionHandlerMiddleware>();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
// TODO replace with another file provider
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "Media")),
RequestPath = "/Media"
});
app.MapControllers();
app.Run();
The Blazor web app's API call:
public async Task<LoginResponseDTO> Login(LoginRequestDTO loginRequestDto)
{
var path = $"{apiOptions.Value.Version}{apiOptions.Value.LoginPath}";
return await HttpPost<LoginRequestDTO, LoginResponseDTO>(loginRequestDto, path);
}
private async Task<TResponse> HttpPost<TRequest, TResponse>(TRequest request, string path)
{
var httpClient = apiHttpClient.CreateClient(ApiOptions.ApiTitle);
var requestMessage = new HttpRequestMessage()
{
Method = HttpMethod.Post,
RequestUri = new Uri(httpClient.BaseAddress, path),
Content = JsonContent.Create(request)
};
var response = await httpClient.SendAsync(requestMessage); // ERROR IS THROWN HERE
return await response.Content.ReadFromJsonAsync<TResponse>();
}
And the web app's Program.cs:
var builder = WebApplication.CreateBuilder(args);
var logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("Logs/SocialMediaAppWeb.txt", rollingInterval: RollingInterval.Day)
.MinimumLevel.Debug()
.CreateLogger();
builder.Logging.ClearProviders();
builder.Logging.AddSerilog(logger);
// Preconfigure an HttpClient for web API calls
builder.Services.AddOptions<ApiOptions>()
.Bind(builder.Configuration.GetRequiredSection(ApiOptions.ApiTitle))
.ValidateDataAnnotations()
.ValidateOnStart();
var apiBaseAddress = builder.Configuration[$"{ApiOptions.ApiTitle}:BaseAddress"]
?? throw new InvalidOperationException($"Configuration value '{ApiOptions.ApiTitle}:BaseAddress' not found.");
builder.Services.AddHttpClient($"{ApiOptions.ApiTitle}", client =>
{
client.BaseAddress = new Uri(apiBaseAddress);
});
builder.Services.AddScoped<IApiService, HttpApiService>();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<IFormFactor, FormFactor>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(SocialMediaApp.Shared._Imports).Assembly);
app.Run();
The exception thrown on the API side with preceding logs:
[DBG] A data reader for 'Auth' on server 'host.docker.internal,1433' is being disposed after spending 0ms reading results.
[DBG] Closing connection to database 'Auth' on server 'host.docker.internal,1433'.
[DBG] Closed connection to database 'Auth' on server 'host.docker.internal,1433' (0ms).
[VRB] Performing protect operation to key {redacted} with purposes ('/app/', 'Microsoft.AspNetCore.Authentication.BearerToken', 'Identity.Bearer', 'RefreshToken').
[INF] UserController.Login: Login success: c042c020-4d9e-458e-b913-08fd9573940d - Succeeded
[INF] Executed action method SocialMediaApp.API.Controllers.UserController.Login (SocialMediaApp.API), returned result Microsoft.AspNetCore.Mvc.HttpActionResult in 718.8738ms.
[VRB] Action Filter: After executing OnActionExecutionAsync on filter SocialMediaApp.API.Attributes.ValidateModelAttribute.
[VRB] Action Filter: After executing OnActionExecutionAsync on filter Asp.Versioning.ReportApiVersionsAttribute.
[VRB] Action Filter: Before executing OnActionExecuted on filter Microsoft.AspNetCore.Mvc.Infrastructure.ModelStateInvalidFilter.
[VRB] Action Filter: After executing OnActionExecuted on filter Microsoft.AspNetCore.Mvc.Infrastructure.ModelStateInvalidFilter.
[VRB] Action Filter: Before executing OnActionExecuted on filter Microsoft.AspNetCore.Mvc.ModelBinding.UnsupportedContentTypeFilter.
[VRB] Action Filter: After executing OnActionExecuted on filter Microsoft.AspNetCore.Mvc.ModelBinding.UnsupportedContentTypeFilter.
[VRB] Result Filter: Before executing OnResultExecuting on filter Microsoft.AspNetCore.Mvc.Infrastructure.ClientErrorResultFilter.
[VRB] Result Filter: After executing OnResultExecuting on filter Microsoft.AspNetCore.Mvc.Infrastructure.ClientErrorResultFilter.
[VRB] Result Filter: Before executing OnResultExecutionAsync on filter Asp.Versioning.ReportApiVersionsAttribute.
[VRB] Result Filter: Before executing OnResultExecutionAsync on filter SocialMediaApp.API.Attributes.ValidateModelAttribute.
[VRB] Before executing action result Microsoft.AspNetCore.Mvc.HttpActionResult.
[INF] Setting HTTP status code 200.
[VRB] After executing action result Microsoft.AspNetCore.Mvc.HttpActionResult.
[VRB] Result Filter: After executing OnResultExecutionAsync on filter SocialMediaApp.API.Attributes.ValidateModelAttribute.
[VRB] Result Filter: After executing OnResultExecutionAsync on filter Asp.Versioning.ReportApiVersionsAttribute.
[VRB] Result Filter: Before executing OnResultExecuted on filter Microsoft.AspNetCore.Mvc.Infrastructure.ClientErrorResultFilter.
[VRB] Result Filter: After executing OnResultExecuted on filter Microsoft.AspNetCore.Mvc.Infrastructure.ClientErrorResultFilter.
[INF] Executed action SocialMediaApp.API.Controllers.UserController.Login (SocialMediaApp.API) in 776.6978ms
[INF] Executed endpoint 'SocialMediaApp.API.Controllers.UserController.Login (SocialMediaApp.API)'
[ERR] An unhandled exception has occurred while executing the request.The response has already started, the error page middleware will not be executed.
Connection id "0HN4M86HTJ4O4", Request id "0HN4M86HTJ4O4:00000002": An unhandled exception was thrown System.InvalidOperationException: StatusCode cannot be set because the response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value)
at Microsoft.AspNetCore.Http.HttpResults.Ok1.ExecuteAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeNextResultFilterAsyncTFilter,TFilterAsync
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext) at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)[DBG] 'AuthDbContext' disposed.
[DBG] Disposing connection to database 'Auth' on server 'host.docker.internal,1433'.
[DBG] Disposed connection to database '' on server '' (0ms).
[INF] Request finished HTTP/1.1 POST https://localhost:44387/api/v0.1/user/login - 200 null application/json; charset=utf-8 957.337ms
And finally, the exception thrown by the Blazor web app:
[INF] Start processing HTTP request POST https://localhost:44387/api/v0.1/user/login
[VRB] Request Headers: Content-Type: application/json; charset=utf-8
[INF] Sending HTTP request POST https://localhost:44387/api/v0.1/user/login
[VRB] Request Headers: Content-Type: application/json; charset=utf-8
[DBG] Rendering component 31 of type Microsoft.AspNetCore.Components.Forms.ValidationSummary
[DBG] Rendering component 33 of type Microsoft.AspNetCore.Components.Forms.ValidationMessage1[System.String]
[DBG] Rendering component 35 of type Microsoft.AspNetCore.Components.Forms.ValidationMessage1[System.String]
[DBG] Rendering component 20 of type SocialMediaApp.Web.Components.Account.Pages.Login
[DBG] Rendering component 26 of type Microsoft.AspNetCore.Components.Forms.Mapping.FormMappingValidator
[DBG] Rendering component 28 of type Microsoft.AspNetCore.Components.CascadingValue1[Microsoft.AspNetCore.Components.Forms.EditContext]
[DBG] Rendering component 33 of type Microsoft.AspNetCore.Components.Forms.ValidationMessage1[System.String]
[DBG] Rendering component 29 of type Microsoft.AspNetCore.Components.Sections.SectionOutlet+SectionOutletContentRenderer
[INF] Received HTTP response headers after 810.0596ms - 200
[INF] End processing HTTP request after 812.7385ms - 200
[INF] Executed endpoint '/Account/Login (/Account/Login)'
[ERR] An unhandled exception has occurred while executing the request.System.Net.Http.HttpRequestException: Error while copying content to a stream.
System.Net.Http.HttpIOException: The response ended prematurely. (ResponseEnded)
at System.Net.Http.HttpConnection.FillAsync(Boolean async)
at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionResponseContent.g__Impl|6_0(Stream stream, CancellationToken cancellationToken) at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer) --- End of inner exception stack trace --- at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer) at System.Net.Http.HttpClient.g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken) at SocialMediaApp.Shared.Services.HttpApiService.HttpPost[TRequest,TResponse](TRequest request, String path) in H:\VS Projects\SocialMediaWebApp\SocialMediaApp.Web\Services\HttpApiService.cs:line 39 at SocialMediaApp.Shared.Services.HttpApiService.Login(LoginRequestDTO loginRequestDto) in H:\VS Projects\SocialMediaWebApp\SocialMediaApp.Web\Services\HttpApiService.cs:line 87 at SocialMediaApp.Web.Components.Account.Pages.Login.LoginUser() in H:\VS Projects\SocialMediaWebApp\SocialMediaApp.Web\Components\Account\Pages\Login.razor:line 93 at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task) at Microsoft.AspNetCore.Components.Forms.EditForm.HandleSubmitAsync() at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task) at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState) at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState) at Microsoft.AspNetCore.Components.Endpoints.EndpointHtmlRenderer.g__Execute|38_0() at Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context) at Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context) at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<>c.<b__10_0>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Antiforgery.Internal.AntiforgeryMiddleware.InvokeAwaited(HttpContext context) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
I've tried disabling unnecessary middleware, using the ControllerBase
OK response instead of TypedResults
, and just about every other potential solution for this exception I can find. Since Swagger is able to read the response correctly, it seems the error might be caused by the web app somewhere, but I'm not sure where.
Here's additional information from inspecting the request in Wireshark and the web browser dev tools:
_handler=login
&__RequestVerificationToken=####
&Input.UsernameOrEmail=user17
&Input.Password=password
{"usernameOrEmail":"user17","password":"password","rememberMe":false,"useCookies":false,"useSessionCookies":false,"twoFactorCode":null,"twoFactorRecoveryCode":null}
{"tokenType":"Bearer","accessToken":"####","expiresIn":3600,"refreshToken":"####"}
Here's a screenshot of the network traffic from Wireshark (port 51995 is the API, 58015 is the UI server HttpClient, 5153 is the client): https://i.sstatic.net/DdDn81D4.png
I figured out the issue. The error came in two parts:
The ExceptionHandlerMiddleware
was indeed attempting to modify the response after it was being sent. This was caused by an error in my code that wrote to the response if the response has sent, when it should be only if the response has not yet been sent.
I was using signInManager.PasswordSignInAsync()
to authenticate the user within the API's Login endpoint. Upon further research, it seems that this function sends a response itself. Attempting to return data caused issues with returning an IActionResult
, where the PasswordSignInAsync()
response took priority over data included the returned IActionResult
.