Use specific status code error page in area | Asp.net core - asp.net-core

I manage status codes error using
app.UseStatusCodePagesWithRedirects("/StatusCodeError/{0}");
But when an error occurs in the area, the page is redirected out of the area and shows the general error page.
How do I make sure that an area-specific error page is displayed if an error occurs in the area?

There is no extensibility point to modify the behavior of app.UseStatusCodePagesWithRedirects, the location is formatted internally using string.Format and with only one argument passed in the placeholder {0} for the status code. So at the calling time (passing in the location format), we have no understanding about the area which is a route value available only in the context of a request processing. So you can refer to the source for StatusCodePagesExtensions and can see that UseStatusCodePagesWithRedirects just depends on an overload of the extension method StatusCodePagesExtensions.UseStatusCodePages, you can freely copy the code and modify it to make another version of UseStatusCodePagesWithRedirects that accepts a location format which supports some arguments from the route values.
Here I've made a simple one for you:
public static class StatusCodePagesApplicationBuilderExtensions
{
public static IApplicationBuilder UseStatusCodePagesWithRouteDataAndRedirects(this IApplicationBuilder app, string locationFormat)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var shouldPrependPathBase = locationFormat.StartsWith("~");
locationFormat = shouldPrependPathBase ? locationFormat.Substring(1) : locationFormat;
return app.UseStatusCodePages(context =>
{
//here is the point we can evaluate for the actual location
//by replacing the placeholders with route value
var location = Regex.Replace(locationFormat, #"\{[^}]+\}", m => {
//ignore the placeholder for status code
if (m.Value == "{0}") return m.Value;
var routeKey = m.Value.Trim('{', '}');
var routeValue = context.HttpContext.GetRouteValue(routeKey);
return routeValue?.ToString();
});
location = string.Format(CultureInfo.InvariantCulture, location, context.HttpContext.Response.StatusCode);
location = (shouldPrependPathBase ? context.HttpContext.Request.PathBase : PathString.Empty) + location;
context.HttpContext.Response.Redirect(location);
return Task.CompletedTask;
});
}
}
Now you can include any route data in the location format, in your case you just need the area, so the code will be like this:
app.UseStatusCodePagesWithRouteDataAndRedirects("/{area}/StatusCodeError/{0}");
NOTE: be careful with redirecting, you may encounter an error saying something like too many redirects, that's because of redirecting to a URL which in return causes a loop of redirects.

Related

How to return a status code from an endpoint that can then be handled by app.UseStatusCodePages() middleware?

If I return StatusCode(403) or any other error code from an endpoint, any configuration of app.UseStatusCodePages<whatever> will be ignored.
I believe this is because the StatusCode(<whatever>) will automatically create a result object, and UseStatusCodePages only kicks in if there is an error status code and no content.
So how do I set a status code result in an IActionResult type endpoint and then return without setting any content so that UseStatusCodePages will handle the job of providing a suitable resonse?
As far as I know, the UseStatusCodePages will just be fired when the action result is the StatusCodeResult.
If you put some value inside the status codes, it will return the object result which will not trigger the UseStatusCodePages.
So I suggest you could directly use StatusCodeResult(403), then if you want to put some value to the StatusCodeResult, I suggest you could put it inside the httpcontext's item.
More details, you could refer to below codes:
public IActionResult OnGet()
{
HttpContext.Items.Add("test","1");
return StatusCode(403);
}
Program.cs:
app.UseStatusCodePages(async statusCodeContext =>
{
var status = statusCodeContext.HttpContext.Items["test"];
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = Text.Plain;
await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
Result:
The issue was that I have the ApiController attribute on the endpoint controller. One of the things this attribute does is to automatically create a ProblemDetails response body for any failed requests, and it is this that prevents UseStatusCodePages from having any effect.
The solution is to either remove the ApiController attribute if you do not require any of its features, or alternatively its behaviour of automatically creating ProblemDetails responses can be disabled using the following configuration in Program.cs (or Startup.cs in old style projects).
builder.Services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
options.SuppressMapClientErrors = true;
});

Adding a letter at the end of an encoded URL causes 404

This hits the corresponding method:
https://localhost:44379/annotation/getbytarget/https%3a%2f%2f
whereas this one causes 404:
https://localhost:44379/annotation/getbytarget/https%3a%2f%2fa
The only difference between the two URLs is that the latter has one extra letter (a) at the end. I am calling these URLs from the browser. But firstly, I had tried HttpClient's GetStringAsync method. No difference.
My goal is to call this URL:
https://localhost:44379/annotation/getbytarget/https://analyzer.app/analysis/60be725a980f947a351e2e97
I am encoding this URL so it becomes:
https://localhost:44379/annotation/getbytarget/https%3a%2f%2fanalyzer.app%2fanalysis%2f60be725a980f947a351e2e97
What could be the reason for this error?
This is an ASP.NET Core 3.1 Web API application and the corresponding Web API method is as follows:
[HttpGet("{id}")]
public ActionResult<List<Annotation>> GetByTarget(string id)
{
var annotations = _annotationService.GetByTarget(id);
if (annotations == null)
{
return NotFound();
}
return annotations;
}
I have solved the issue by changing the Web API method's routing signature (I am not sure if routing signature is the correct term). Notice that I have commented out the [HttpGet("{id}")] attribute:
//[HttpGet("{id}")]
public ActionResult<List<Annotation>> GetByTarget(string id)
{
var annotations = _annotationService.GetByTarget(id);
if (annotations == null)
{
return NotFound();
}
return annotations;
}
Now the method accepts calls this way (notice the id parameter):
https://localhost:44379/annotation/getbytarget?id=https%3a%2f%2fanalyzer.app%2fanalysis%2f

Getting a client readable message from an Npgsql.PostgresException

I'm writing a web api using PostgreSQL and am checking database constraints as part of the validation process, but I also have a global exception filter as a fallback in case something gets by when saving. My problem is that the exception doesn't seem to have any message that I can present to the client without some processing. The added image is of the PostgresException data from a breakpoint. For example, in this case I would want something along the lines of "Asset Number x already exists" or just "Asset Number must be unique". Is this something that can be configured somewhere? The place that makes the most sense is at the constraint creation code, but I couldn't find an option to do so.
modelBuilder.Entity<AssetItem>().HasIndex(item => new { item.AssetNumber }).IsUnique();
public class DbExceptionFilter : IExceptionFilter
{
private const string UNIQUE_EXCEPTION = "23505";
public async void OnException(ExceptionContext context)
{
var exceptionType = context.Exception.InnerException.GetType().FullName;
if (exceptionType == "Npgsql.PostgresException")
{
var pgException = (PostgresException) context.Exception.InnerException;
switch(pgException.SqlState)
{
case UNIQUE_EXCEPTION:
var error = new {error = "Unique Error Here"};
await WriteJsonErrorResponse(context.HttpContext.Response, HttpStatusCode.BadRequest, error);
return;
}
}
else
{
var error = new { error = "Unexpected Server Error"};
await WriteJsonErrorResponse(context.HttpContext.Response, HttpStatusCode.InternalServerError, error);
return;
}
}
private async Task WriteJsonErrorResponse(HttpResponse response, HttpStatusCode statusCode, dynamic error)
{
response.ContentType = "application/json";
response.StatusCode = (int) statusCode;
await response.Body.WriteAsync(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(error)));
}
}
The closest thing to a user-readable message that PostgreSQL provides is the message text exposed on PostgresException.
However, as a general rule it is not a good idea to expose database errors directly to users (including web API users): these are intended to the application directly interacting with the database (i.e. your application). These messages generally don't mean much to the users of your API, and more importantly they leak potentially sensitive information about your database schema and are therefore not secure. It's especially problematic to dump/serialize the entire exception to the user as you seem to be doing (with JsonConvert.SerializeObject).
The best practice here would be to identify legitimate database exceptions that the user may trigger, intercept these and return and appropriately-worded message of your own (e.g. "A user with that name already exists").
As a side note, to identify PostgresException, rather than getting the name of the exception and comparing to that, you can simply use C# pattern matching:
if (context.Exception.InnerException is PostgresException postgresException)
{
// ...
}

Understanding seam filter url-pattern and possible conflicts

I made a custom editor plugin, in a Seam 2.2.2 project, which makes file upload this way:
1) config the editor to load my specific xhtml upload page;
2) call the following method inside this page, and return a javascript callback;
public String sendImageToServer()
{
HttpServletRequest request = ServletContexts.instance().getRequest();
try
{
List<FileItem> items = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request);
processItems(items);//set the file data to specific att
saveOpenAttachment();//save the file to disk
}
//build callback
For this to work I have to put this inside components.xml:
<web:multipart-filter create-temp-files="false"
max-request-size="1024000" url-pattern="*"/>
The attribute create-temp-files do not seems to matter whatever its value.
But url-pattern has to be "" or "/myUploadPage.seam", any other value makes the item list returns empty. Does Anyone know why?
This turns into a problem because when I use a url-pattern that work to this case, every form with enctype="multipart/form-data" in my application stops to submit data. So I end up with other parts of the system crashing.
Could someone help me?
To solve my problem, I changed the solution to be like Seam multipart filter handle requests:
ServletRequest request = (ServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
try
{
if (!(request instanceof MultipartRequest))
{
request = unwrapMultipartRequest(request);
}
if (request instanceof MultipartRequest)
{
MultipartRequest multipartRequest = (MultipartRequest) request;
String clientId = "upload";
setFileData(multipartRequest.getFileBytes(clientId));
setFileContentType(multipartRequest.getFileContentType(clientId));
setFileName(multipartRequest.getFileName(clientId));
saveOpenAttachment();
}
}
Now I handle the request like Seam do, and do not need the web:multipart-filter config that was breaking other types of request.

Specifyng a default message for Html.ValidationMessageFor in ASP.NET MVC4

I want to display an asterisk (*) next to a text box in my form when initially displayed (GET)
Also I want to use the same view for GET/POST when errors are present) so For the GET request
I pass in an empty model such as
return View(new Person());
Later, when the form is submitted (POST), I use the data annotations, check the model state and
display the errors if any
Html.ValidationMessageFor(v => v.FirstName)
For GET request, the model state is valid and no messages, so no asterisk gets displayed.
I am trying to workaround this by checking the request type and just print asterisk.
#(HttpContext.Current.Request.HttpMethod == "GET"? "*" : Html.ValidationMessageFor(v=> v.FirstName).ToString())
The problem is that Html.ValidationMessageFor(v=> v.FirstName).ToString() is already encoded
and I want to get the raw html from Html.ValidationMessageFor(v=> v.FirstName)
Or may be there is a better way here.
1. How do you display default helpful messages (next to form fields) - such as "Please enter IP address in the nnn.nnn.nnn.nnn format) for GET requests and then display the errors if any for the post?
2. What is the best way from a razor perspective to check an if condition and write a string or the MvcHtmlString
Further to my last comment, here is how I would create that helper to be used:
public static class HtmlValidationExtensions
{
public static MvcHtmlString ValidationMessageForCustom<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, string customString)
{
var returnedString = HttpContext.Current.Request.HttpMethod == "GET" ? customString : helper.ValidationMessageFor(expression).ToString();
return MvcHtmlString.Create(returnedString);
}
}
And it would be used like this #Html.ValidationMessageForCustom(v=> v.FirstName, "Please enter IP address in the nnn.nnn.nnn.nnn format")