Fix error propagation: surface API validation messages in the UI

- UseJsonExceptionHandler now maps InvalidOperationException to 400 (was 500),
  so upstream business-rule rejections reach the browser as actionable messages.
- CvMatcherController forwards Refit 4xx bodies from cv-matcher-api instead
  of swallowing them in a generic 502.
- ErrorResponse.Score removed; CaptchaController puts the score in Detail.
- Frontend extractApiError helper reads the server Error/error/title field for
  4xx responses and falls back to a generic i18n string for 5xx / missing body.
- All four failure handlers (CV upload, CV match, contact form, subscribe form)
  updated to use extractApiError with the correct rate-limit i18n key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 09:41:24 +03:00
parent 2e1efc598b
commit 7908dad181
5 changed files with 80 additions and 15 deletions
+23 -2
View File
@@ -191,14 +191,35 @@ public static class StartupExtensions
{
var feature = context.Features.Get<IExceptionHandlerFeature>();
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(serviceName);
context.Response.ContentType = "application/json";
// InvalidOperationException signals an intentional business-rule violation
// (e.g. "Could not extract enough job text"). Surface it as 400 with the
// original message so the caller can show it directly to the user.
if (feature?.Error is InvalidOperationException ioe)
{
logger.LogWarning(ioe, "Business rule violation in {Service}", serviceName);
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new Common.Responses.ErrorResponse
{
Error = ioe.Message,
Code = "validation_error"
});
return;
}
if (feature?.Error is not null)
{
logger.LogError(feature.Error, "Unhandled exception in {Service}", serviceName);
}
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new { error = "Unexpected server error." });
await context.Response.WriteAsJsonAsync(new Common.Responses.ErrorResponse
{
Error = "Unexpected server error.",
Code = "internal_error"
});
});
});
}