Pinqponq.ErrorHandling
1.2.0
Prefix Reserved
dotnet add package Pinqponq.ErrorHandling --version 1.2.0
NuGet\Install-Package Pinqponq.ErrorHandling -Version 1.2.0
<PackageReference Include="Pinqponq.ErrorHandling" Version="1.2.0" />
<PackageVersion Include="Pinqponq.ErrorHandling" Version="1.2.0" />
<PackageReference Include="Pinqponq.ErrorHandling" />
paket add Pinqponq.ErrorHandling --version 1.2.0
#r "nuget: Pinqponq.ErrorHandling, 1.2.0"
#:package Pinqponq.ErrorHandling@1.2.0
#addin nuget:?package=Pinqponq.ErrorHandling&version=1.2.0
#tool nuget:?package=Pinqponq.ErrorHandling&version=1.2.0
Pinqponq.ErrorHandling
Global exception-handling middleware for ASP.NET Core, plus a standard, camelCase
ErrorResponse contract. Every unhandled exception becomes a consistent JSON error
body and a single structured log record — using field names compatible with
Pinqloq's log pipeline — instead of an ad-hoc stack trace or a framework default
problem-details page.
Install
dotnet add package Pinqponq.ErrorHandling
Requirements
- .NET 8.0, 9.0, or 10.0
- ASP.NET Core (the package uses
<FrameworkReference Include="Microsoft.AspNetCore.App" />, so it targets web applications, not plain console/worker apps)
Quick start
using Pinqponq.ErrorHandling;
using Pinqponq.ErrorHandling.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddPinqponqErrorHandling();
var app = builder.Build();
// Register first, so it wraps every other middleware and can catch
// exceptions raised anywhere downstream in the pipeline.
app.UsePinqponqErrorHandling();
app.MapGet("/orders/{id}", (int id) =>
{
throw new KeyNotFoundException($"Order {id} was not found.");
});
app.Run();
A request to the endpoint above returns HTTP 404 with:
{
"status": false,
"statusCode": 404,
"message": "The requested resource was not found.",
"responseCode": "not_found",
"traceId": "0HN7...:00000001"
}
Configuration
AddPinqponqErrorHandling(Action<ErrorHandlingOptions>? configure = null) registers
ErrorHandlingOptions (unvalidated — there's no required field). UsePinqponqErrorHandling()
adds ExceptionHandlingMiddleware to the pipeline via app.UseMiddleware<...>().
Call UsePinqponqErrorHandling() as early as possible in the pipeline — ideally
the very first app.Use... call — so it wraps everything else and can catch
exceptions from routing, authentication, and every other middleware/endpoint
downstream of it.
| Option | Default | Notes |
|---|---|---|
CorrelationIdHeader |
"X-Correlation-ID" |
Inbound request header read to resolve a correlation id. |
IncludeExceptionMessage |
false |
When false (recommended for production), the response message is a generic, status-derived string (e.g. "The requested resource was not found."), never the raw Exception.Message — so internal details never leak to a client. Set to true only in trusted/diagnostic environments. |
StatusMappingResolver |
null |
Optional Func<Exception, ExceptionStatusMapping?> to override the built-in status/response-code mapping for specific exception types. Return null from the delegate to fall back to the built-in mapping for that exception. If the resolver itself throws, the error is logged and the built-in mapping is used. |
Example with a custom mapping for a domain exception:
builder.Services.AddPinqponqErrorHandling(options =>
{
options.StatusMappingResolver = exception => exception switch
{
OrderAlreadyShippedException => new ExceptionStatusMapping(
StatusCodes.Status409Conflict, "order_already_shipped"),
_ => null,
};
});
Localized error responses
builder.Services.AddPinqponqErrorHandling();
builder.Services.AddPinqponqErrorLocalization();
With localization on, the response code from the status mapping above (or from an
IExceptionResolver) is looked up in the message catalog and the message is returned in the
language of the LanguageHeader request header (Accept-Language by default; tr, de-DE, … —
case-insensitive). A mapped code the catalog does not know (not_found, …) is kept and gets the
FallbackResponseCode (general_error) message; a resolver code the catalog does not know becomes
FallbackResponseCode. The status always comes from the mapping or the resolver.
ErrorLocalizationOptions |
Default | Notes |
|---|---|---|
LanguageHeader |
Accept-Language |
Header carrying the language; its first tag is used, and pt-BR falls back to PT when there is no PT-BR catalog. |
DefaultLanguage |
EN |
Used when the header is missing or the catalog has no message in that language. |
FallbackResponseCode |
general_error |
Code for an unknown resolver code; its message also serves an unknown mapped code. |
DefaultMessage |
An unexpected error occurred. |
Used when the catalog has no message at all. |
CatalogDirectory |
Localization |
The application's catalog, relative to AppContext.BaseDirectory: one <LANGUAGE>.json per language, each a { "code": "message" } object. Its entries add codes and override the embedded ones. A file that cannot be read is logged and skipped. |
CatalogAssemblies |
this package | Assemblies whose embedded *.Localization.<LANGUAGE>.json resources form the built-in catalog. This package ships general_error, api_body_required, api_page_size_invalid, api_page_number_invalid and too_many_requests in AR, DE, EN, ES, FR, HI, ID, IT, JA, KO, PT and TR; Pinqponq.ErrorHandling.Postgres adds the database_* codes. |
Exception resolvers
An IExceptionResolver handles the exceptions of one technology before StatusMappingResolver
and the built-in table. Register it with AddPinqponqExceptionResolver<T>(); the first resolver
that returns an ExceptionResolution wins, and a resolver that throws is logged and skipped.
An ExceptionResolution names the status and response code, an optional FallbackResponseCode, the ReportedException that names the log event (e.g.
the driver exception inside an ORM exception) and extra LogDetail fields.
With localization on, a resolver code the catalog does not know becomes the resolution's
FallbackResponseCode, or the options' one.
Pinqponq.ErrorHandling.Postgres is one.
Writing an error without an exception
AddPinqponqErrorHandling also registers IErrorResponseWriter. Use it when code answers a
failure itself instead of throwing, for example a rejected rate limit (Pinqponq.RateLimiting uses
it):
await errorResponseWriter.Write(
context,
StatusCodes.Status429TooManyRequests,
CommonErrorCodes.TooManyRequests,
defaultMessage: "Too many requests. Try again later.",
eventName: "request_rate_limited");
It writes the same body as the middleware and describes the failure on IRequestErrorFeature
under eventName. With localization on, the message comes from the catalog in the request's
language, and a code the catalog does not know is kept and gets the FallbackResponseCode
message. With localization off, defaultMessage is written.
Request error feature
Every answered failure — a handled exception or a request the client closed — is
described on HttpContext.Features as Pinqponq.Contracts.IRequestErrorFeature:
EventName:exception_<ExceptionType>orrequest_cancelled_by_client(constants inErrorHandlingEvents);Detail:traceId,method,statusCode,errorCode,errorMessage,language(with localization on),queryString(when present), then the resolver'sLogDetail.
Pinqponq.Observability's request logging reads it, so a failed request still produces a single Pinqloq record that carries the error. Nothing is set when the response had already started.
Main types
AddPinqponqErrorHandling/UsePinqponqErrorHandling—IServiceCollection/IApplicationBuilderextensions that register options and the middleware.ExceptionHandlingMiddleware— catches unhandled exceptions from the rest of the pipeline, logs a structured record, and writes theErrorResponseJSON body. Also handles client-disconnect (OperationCanceledExceptionwhenHttpContext.RequestAbortedwas signaled) by setting HTTP 499 without producing an error body; the console log entry for that case isInformation.ErrorHandlingOptions— configuration described above.ErrorResponse— the response contract, serialized camelCase:status(alwaysfalse),statusCode,message,responseCode,traceId.ExceptionStatusMapping— areadonly record struct(int StatusCode, string ResponseCode)returned by a customStatusMappingResolver.ExceptionMapping(internal) — the built-in exception → (status code, response code) table:Exception type Status Response code UnauthorizedAccessException401 unauthorizedKeyNotFoundException404 not_foundArgumentException(and subtypes, e.g.ArgumentNullException)400 bad_requestFormatException400 bad_requestInvalidOperationException400 bad_requestNotImplementedException501 not_implementedTimeoutException504 timeoutAny type whose name contains "NotFound"404 not_foundEverything else 500 internal_errorOperationCanceledException(not aborted by the client) is mapped separately, to 504 /timeout, before this table is even consulted.
Notes / behavior
- Response
messageis generic by default. WithIncludeExceptionMessage = false(the default), the body'smessagecomes from a small status-code-keyed table of user-safe strings (e.g. 400 → "The request was invalid.", 401 → "Authentication is required.", 500 → "An unexpected error occurred."), never fromexception.Message. - Correlation id / trace id —
X-Correlation-IDbehavior. The middleware resolves a correlation id per request: if the inbound request has a non-emptyCorrelationIdHeader(defaultX-Correlation-ID), that header's value is used; otherwise it falls back toHttpContext.TraceIdentifier. The response body'straceIdfield is set to this resolved correlation id. The structured log record additionally includes the request's ownTraceId(alwaysHttpContext.TraceIdentifier, regardless of the header) as a separate field. This means:- No
X-Correlation-IDheader sent → responsetraceIdand the log'sTraceIdare the same value (bothHttpContext.TraceIdentifier). X-Correlation-IDheader sent → responsetraceIdand the log'sCorrelationIdfield both equal the header value, while the log's ownTraceIdfield remains the request's ASP.NET CoreTraceIdentifier— the two intentionally differ, so you can still find the record by either the caller-supplied correlation id or the request's native identity.
- No
- Structured logging is Pinqloq-compatible. Each handled exception produces
exactly one log entry (
LogLevel.Errorfor 5xx,LogLevel.Warningfor 4xx) with a message template and named placeholders —{ResponseCode},{StatusCode},{Method},{Path},{TraceId},{CorrelationId}— rather than an interpolated string, so the fields remain queryable/structured in whatever log sink is configured (Pinqloq or otherwise). - The response is only written if it hasn't started. If
HttpContext.Response.HasStartedis alreadytrue(e.g. streaming had begun), the middleware still logs the exception but skips writing a body — the connection is left as-is rather than throwing a second exception trying to write over a started response. - A failure to write the error response is itself logged (
LogError) rather than allowed to propagate, so a serialization or I/O problem while producing the error body cannot itself crash the pipeline a second time. - Client disconnects are not treated as errors. A canceled request
(
OperationCanceledExceptionwhileRequestAborted.IsCancellationRequested) sets HTTP 499 (if the response hasn't started) and produces noErrorResponsebody and only anInformationlog entry — this is expected client behavior, not a server fault.
Related packages
This package depends only on Pinqponq.Contracts (for IRequestErrorFeature and
CommonErrorCodes). It is commonly combined with Pinqponq.Observability (request logging that
carries the error detail) and Pinqponq.ErrorHandling.Postgres (database error mapping).
Samples
Try this package in the browser via Pinqponq.Playground — see samples/README.md.
Repository
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Pinqponq.Contracts (>= 1.1.0)
-
net8.0
- Pinqponq.Contracts (>= 1.1.0)
-
net9.0
- Pinqponq.Contracts (>= 1.1.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Pinqponq.ErrorHandling:
| Package | Downloads |
|---|---|
|
Pinqponq.ErrorHandling.Postgres
PostgreSQL error mapping for Pinqponq.ErrorHandling: turns Npgsql PostgresException (also wrapped in an EF Core DbUpdateException) into database_* error responses (400 for constraint and value errors, 500 otherwise) and localized messages. |
|
|
Pinqponq.RateLimiting
ASP.NET Core rate limiting set up in one call: fixed-window policies partitioned by client address, user or a caller-supplied key (hashed when it is a secret), chained global limits, a 429 rejection with Retry-After and the standard error body, and startup validation. |
GitHub repositories
This package is not used by any popular GitHub repositories.