Files
satellite-provider/SatelliteProvider.Api/GlobalExceptionHandler.cs
T
Oleksandr Bezdieniezhnykh 1d89cd9997
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
[AZ-353][AZ-354][AZ-356] Refactor 03 batch 2: harden API surface
AZ-353: Centralize 500 handling via GlobalExceptionHandler /
AddProblemDetails / UseExceptionHandler. Sanitized ProblemDetails
body carries a generic title, RFC9110 type link, and the request's
TraceIdentifier as correlationId; the leaky exception message stays
server-side in the ERR log entry. Strip per-endpoint
try/catch (Exception) wrappers and the unused ILogger<Program>
parameters they served. Preserve the typed ArgumentException catch
in CreateRoute (AC-3). The handler maps BadHttpRequestException
back to its framework-supplied StatusCode so model-binding /
malformed-body failures stay 4xx instead of being promoted to 500.

AZ-354: Extract CorsConfigurationValidator (pure static helpers)
and wire it into Program.cs. Production with empty
CorsConfig:AllowedOrigins and no CorsConfig:AllowAnyOrigin opt-in
now throws InvalidOperationException at host startup. Development
keeps the permissive default but logs a warning post-build. Adds
the explicit CorsConfig:AllowAnyOrigin escape hatch.

AZ-356: GetSatelliteTilesByMgrs and UploadImage now return
Results.Problem(StatusCode 501) with ProblemDetails. Added
.ProducesProblem(501) so swagger.json documents the
not-implemented status.

Tests: SatelliteProvider.Tests now references SatelliteProvider.Api
(downward, idiomatic) so unit tests can reach the new helpers.
+9 CorsConfigurationValidator unit tests, +3
GlobalExceptionHandler unit tests, +3 StubAndErrorContractTests
integration tests (added to smoke + full suites).

58/58 unit + 5/5 smoke + 3/3 stub-contract pass.
Code review verdict: PASS.
Batch report: _docs/03_implementation/batch_08_report.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 23:52:52 +03:00

77 lines
2.5 KiB
C#

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace SatelliteProvider.Api;
public sealed class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
// Framework-level request-binding/parsing failures carry their own HTTP status
// (typically 400/415). Honor that status so we don't promote a client error to 5xx.
if (exception is BadHttpRequestException badRequest)
{
await WriteClientErrorAsync(httpContext, badRequest, cancellationToken);
return true;
}
var correlationId = httpContext.TraceIdentifier;
_logger.LogError(
exception,
"Unhandled exception while processing {Method} {Path} (correlationId={CorrelationId})",
httpContext.Request.Method,
httpContext.Request.Path,
correlationId);
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
var problem = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "Internal Server Error",
Detail = "An unexpected error occurred. Use the correlationId to look up the server log entry.",
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-500-internal-server-error",
};
problem.Extensions["correlationId"] = correlationId;
await httpContext.Response.WriteAsJsonAsync(
problem,
options: null,
contentType: "application/problem+json",
cancellationToken: cancellationToken);
return true;
}
private static async Task WriteClientErrorAsync(
HttpContext httpContext,
BadHttpRequestException badRequest,
CancellationToken cancellationToken)
{
httpContext.Response.StatusCode = badRequest.StatusCode;
var problem = new ProblemDetails
{
Status = badRequest.StatusCode,
Title = "Bad Request",
Detail = badRequest.Message,
};
await httpContext.Response.WriteAsJsonAsync(
problem,
options: null,
contentType: "application/problem+json",
cancellationToken: cancellationToken);
}
}