I'm having a strange problem with ASP.Net Core custom middleware.
I've got an exception handler that I'm trying to log exceptions and then send a generic message to the caller without the exception details that seem to be sent by default.
The problem is that the response is being sent without a body.
My code:
public async Task Invoke(HttpContext context)
{
try
{
await next.Invoke(context);
}
catch (Exception e)
{
logger.LogError(e, "Request threw an exception");
context.Response.StatusCode = 500;
using (var writer = new StreamWriter(context.Response.Body))
{
await writer.WriteAsync($"Error on server processing request");
await writer.FlushAsync();
}
}
}
I'm using .Net 5. This project is a Web API project, in case that makes any difference.
I'm invoking my API via swagger UI and I'm also checking the responses using the dev tools in Edge (Chromium version) to make sure that swagger isn't hiding the body.
It appears that there is no body to the response, it's just an empty response with a 500 code.
Running in the debugger shows that it is executing the code and if I change the response code to 566 that response code is received by SwaggerUI so it's doing something.
Note: before I added the FlushAsync() call, I was getting an exception sent to Swagger saying that the dispose was using a synchronous write when flushing so that seems to be necessary.
Update:
Pipeline configuration, as requested:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment() || env.IsTesting())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Clients v1"));
}
app.UseRequestLoggingMiddleware(); // My custom middleware
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Behaviour is the same regardless of the environment (development, testing, staging or production)
I'm uncertain why using context.Response.Body is not working, but what I've done instead is use the methods directly on context.Response:
context.Response.StatusCode = 500;
await context.Response.WriteAsync($"Error on server processing request");
Update: Given your middleware pipeline starts with app.UseDeveloperExceptionPage() I suspect that's what's overwriting the response - at least if you're running with dev or test as environment. This is the middleware that actually exposes the full exception details you are saying you're trying to avoid. In the ASP.NET Core project boilerplate/template this is intentionally only added when not running on a production environment.
So perhaps your problem will be solved by changing the ASPNETCORE_ENVIRONMENT environment variable to something other than Development or Test. Or if you still want your own middleware, you should probably remove app.UseDeveloperExceptionPage() and perhaps even move your own app.UseRequestLoggingMiddleware() up as the first line in Configure (although I don't think the Swagger stuff should interfere - but I make no promises :) )
I found the problem. The code I needed was
context.Response.StatusCode = 500;
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync($"Error on server processing request");
I noticed that if I requested the API by typing the url into the web browser, I was getting the text back but when I requested via Swagger UI, it was not sending the text.
SwaggerUI was setting an accept: text/plain header with the request so ASP.Net was ignoring any content that wasn't set as this type (the ContentType is null by default).
The browser had */* in its accept header so the content was being sent regardless of type.
Related
Can we use status code pages middleware to log 400-599 errors in .NET Core Web API App? In Microsoft docs, the option of UseStatusCodePages is given as a common approach for handling errors in web apps. They have not mentioned it in the doc for handling errors in web API apps. Will there be any issue with using Status Code Pages middleware in web API apps when we deploy the app to the cloud? We want to return a 'Problem' response for all errors 400-599. The reason I asked for StatusCodePages is that there are certain status codes that do not raise exceptions and hence are not caught by the Exception Handler Middleware. E.g. 431 - Request Headers too long. Apparently, Status Code Pages should take care of such status codes' responses.
You could, and it may work for your use case (e.g. If things are blowing up, the text in the response may not matter to you).
However, the reason they are not mentioned in an API context is that it would be bad form for an API that returns JSON only response to start responding with plain text error pages. It's better if you return a JSON response, something like :
{
"errorCode" : "500",
"errorMessage" : "Something went wrong"
}
Which is what you do see with things like the inbuild model validation.
You could follow the offcial document:https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?wt.mc_id=docsexp4_personal-blog-marouill&view=aspnetcore-5.0
In startup:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseExceptionHandler("/error-local-development");
}
else
{
app.UseExceptionHandler("/error");
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
ErrorController:
[ApiController]
public class ErrorController : ControllerBase
{
[Route("/error-local-development")]
public IActionResult ErrorLocalDevelopment(
[FromServices] IWebHostEnvironment webHostEnvironment)
{
if (webHostEnvironment.EnvironmentName != "Development")
{
throw new InvalidOperationException(
"This shouldn't be invoked in non-development environments.");
}
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
return Problem(
detail: context.Error.StackTrace,
title: context.Error.Message);
}
[Route("/error")]
public IActionResult Error() => Problem();
}
My test result:
I'm using a Gzip middleware, which decompresses "application/gzip" requests and changes them to "application/json". It works in .Net 3.1 and 5, but in .Net 6 (Minimal API) I get status 415 (Unsupported media type) response. The middleware remained unchanged, in a seperate project (.net standard 2.1). In debugger everything seems to work at first, request is processed by the middleware as always and passed further. I tried sending a normal json with only content-type header changed, and got the same error.
UPDATE: I found more middleware code. I need to process the requests with "content-type": "application/gzip" because that is the format sent by a lot of legacy devices communicating with the api.
Program.cs:
//... Services ...//
#region Middleware
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.All
});
app.UseRouting();
app.UseAuthorization();
app.UseGzipRequestDecompression(); // My custom middleware
app.UseGlobalExceptionHandler();
#endregion //Middleware
Middleware:
public async Task InvokeAsync(HttpContext httpContext)
{
var request = httpContext.Request;
if (request.ContentType == "application/gzip")
{
using (var gzipStream = new GZipStream(request.Body, CompressionMode.Decompress))
{
await gzipStream.CopyToAsync(outputStream);
}
outputStream.Seek(0, SeekOrigin.Begin);
// Replace request content with the newly decompressed stream
request.Body = outputStream;
// Change request type to json
request.ContentType = "application/json";
}
await _next(httpContext);
}
As suggested by #JeremyLakeman, the problem was in .UseRouting.
According to microsoft documentation:
Apps typically don't need to call UseRouting or UseEndpoints. WebApplicationBuilder configures a middleware pipeline that wraps middleware added in Program.cs with UseRouting and UseEndpoints. However, apps can change the order in which UseRouting and UseEndpoints run by calling these methods explicitly.
The solution was to move the .UseRouting after the middleware (and not to delete it), so it looked like this:
app.UseAuthorization();
app.UseGzipRequestDecompression();
app.UseGlobalExceptionHandler();
app.UseRouting();
#endregion //Middleware
edit 00: NOTE:
This message is coming from the GraphiQL interface. When I tried the same query in the 'Banana Cake Pop' UI, no message is returned when I execute this query.
While trying to create a GraphQL subscription on an ASP.NET web server, using Hot Chocolate, with exactly the same code found in this tutorial, I am receiving error feedback from the server.
Error Message
{
"errors": [
{
"message": "Result type not supported.",
"extensions": {
"code": "RESULT_TYPE_NOT_SUPPORTED"
}
}
]
}
I have tried to recreate the tutorial exactly and it's not working. I am also unable to get any of the examples working from these examples. It's only with subscriptions though, queries and mutations are all working perfectly fine.
Services Configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
// [GRAPHQL]
services.AddInMemorySubscriptionProvider();
services.AddGraphQL(SchemaBuilder.New()
.AddQueryType<ShuttleQuery>()
.AddMutationType<ShuttleMutation>()
.AddType<Subscription>()
.BindClrType<string, StringType>()
.Create()
);
}
Application Configuration
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// 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.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
});
// [GRAPHQL]
app.UseWebSockets().UseGraphQL("/graphql");
app.UseGraphQL("/graphql");
}
Generic subscription type based on the tutorial
public class Subscription
{
[SubscribeAndResolve]
public async IAsyncEnumerable<string> OnMessageAsync()
{
yield return "Hey!";
await Task.Delay(2000);
yield return "It Changed?";
await Task.Delay(2500);
yield return "It Never Changes Because It Doesn't W";
}
}
I have been stuck on this for some days now, any help would be greatly appreciated.
I had the same error when try to call subscription from code(FE: react, BE: .net core). And probelm was that I call subscription via http... Subscription works via WebSocket.
So check if you app call subscription wia WEbSocket.
Here you can see how to configure client to split request between http and ws : https://github.com/howtographql/react-apollo/blob/master/src/index.js
The question topic error message only shows up in GraphiQL
TL;DR:
I resolved this issue by using a .Net Core 3.0 Console Application, instead of a ASP.NET Web Application Project Type.
Even though GraphiQL is the only editor which returns the above message, the problem remains regardless of how you try to consume the subscription. It just lacks an error message in other editors.
There must be some sort of background configuration I am not aware of (I am fairly new to C#/.NET). I was running the example star wars project files in a .NET Core Console Application, and an ASP.NET Web Application. They had the same files, correct name spacing, successful build, mutations and queries working in both projects.
But only the console project allows subscriptions.
I don't truly know how to resolve this issue except to start your root project as a .NET Core 3.0 Console Application, should someone else know I would love to have a better understanding of what this issue really is.
I'm trying to get a copy of a response after my MVC controller action has executed but from other questions on here I can't get this code working (even though this appeared to be a well answered problem) ...
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Use(async (context, next) =>
{
var resultStream = context.Response.Body;
context.Response.Body = new MemoryStream();
// allow the response to be written in future request lifecycle events
await next.Invoke();
// fetch the repsonse
context.Response.Body.Seek(0, SeekOrigin.Begin);
var headers = context.Response.Headers;
var body = new StreamReader(context.Response.Body).ReadToEnd();
// ... other code omitted for question clarity
// write the response to the client
context.Response.Body.Seek(0, SeekOrigin.Begin);
await context.Response.Body.CopyToAsync(resultStream);
context.Response.Body = resultStream;
});
}
// ... other code omitted for question clarity
}
When I get to the second seek the variable "body" is empty.
Any ideas why this may be the case when the result after this is a page with content ?
So it turns out the problem here is to do with the fact that Owin and asp.net are not intertwined in the full lifecycle together as I had thought.
In short, the request lifecycle looks something like ...
Do Owin stuff (all owin middlewares)
Do MVC stuff
Do server stuff
... what I need is ...
Do owin stuff
Do MVC stuff
Do more Owin stuff
Do server stuff
... of course i'm hugely over simplifying the process here but i guess the short way to explain it is when you do ...
app.Use((context, next) => { ... }).UsestageMarker(?);
... there is no stage marker for "response processing complete".
Interestingly aspnet core gives us much more control as the tight integration with all the pieces throughout the request lifecycle are less dependent on predefined stages and more about you the user defining your own process for handling requests and constructing the response.
In short ... in aspnet core I can do what i'm trying to do but this does not appear to be possible in owin with .net 4.6
I did however a few references to using filters and handling "OnActionExectuted" which if you look you have a completely different Request and Response object than those given to you by owin middleware's pipeline (thus adding more evidence that these things are not in fact one single process but simply two that happen in an order).
I have since spent the time looking at migrating my application to aspnet core ... which is proving to be more of a headache than i had anticipated, but i'm ever hopeful of the final result being cleaner and faster.
I have a dot net core web application with some mvc controllers and an angular 2 app. I am trying to re-write request paths like "www.example.com/old/path" to "www.example.com/new/path" and then send it to the angular 2 app (which has routing setup only for "new/path"). But Angular keeps getting the old path even though re-writing seems to have worked (judging from breakpoints). I suspect this might reflect some kind of gap in my understanding of the order of middleware execution (but I have tried different orders and spamming the re-writing code everywhere to no avail).
This is how the url re-writing middleware looks like (UrlRewritingMiddleware.cs):
public sealed class UrlRewritingMiddleware
{
private readonly RequestDelegate _next;
private readonly string OldPathSegment= "/old/path/";
private readonly string NewPathSegment= "/new/path/";
public UrlRewritingMiddleware(RequestDelegate next)
{
this._next = next;
}
private void RewriteUrl(HttpContext context)
{
if (context.Request.Path.Value.IndexOf(OldPathSegment, 0, StringComparison.CurrentCultureIgnoreCase) != -1)
{
context.Request.Path = new PathString(Regex.Replace(context.Request.Path.Value, OldPathSegment, NewPathSegment, RegexOptions.IgnoreCase));
}
}
public async Task Invoke(HttpContext context)
{
RewriteUrl(context);
await _next.Invoke(context);
if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value))
{
context.Request.Path = "/app/root/index.html";
context.Response.StatusCode = 200;
await _next.Invoke(context);
RewritePathsInContext(context);//spam
}
else
{
//spam
RewritePathsInContext(context);
}
}
}
And then this is how the Startup.cs Configure method looks like:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//putting in the beginning, I get error in angular
//EXCEPTION: Uncaught (in promise): Error: Cannot match any routes. URL Segment: '/old/path'
app.UseUrlRewritingMiddleware();
app.AnotherCustomMiddleware();//I can see Path changed to new/path inside here
app.UseDefaultFiles();
app.UseMvc();
app.UseStaticFiles();
//putting this at the end gives me 404 in asp.net
//app.UseUrlRewritingMiddleware();
}
After a good night's sleep I realized that server side re-writing of the request path is not going to change the url that shows up in the browser (which is what angular gets). So www.example.com/old/path is going to remain the same in the browser and only a redirect can change it to www.example.com/new/path (which is not what I want). To solve the issue I had to add redirects in the angular app itself. My angular app also calls some mvc controllers/ views but they are always in the /new/path format so I don't need server side re-writing at this point.
Also, about the order of middlewares I suspect the reason putting it at the end gave me a 404 in asp.net might be because it comes after the UseMvc middleware (which already sets up routes?). Haven't tested it but as long as it comes before the UseMvc middleware, I think it should work. EDIT: See ssmith's comment below. UseMvc is a terminal middleware so anything after it won't work.
If you do need server side url re-writing, asp.net core has a re-writing middleware that you can use and don't actually need to write your own:
var options = new RewriteOptions()
.AddRewrite(#"/old/path/", "/new/path/", skipRemainingRules: true);
app.UseRewriter(options);