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:
+28
-11
@@ -403,6 +403,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a user-facing error message from a failed API response body.
|
||||
* For 4xx responses the server's error field is shown directly (it is intentional user feedback).
|
||||
* For 5xx or missing body a generic i18n fallback is returned instead.
|
||||
* @param {object|null} body Parsed JSON response body, or null if unparseable.
|
||||
* @param {number} status HTTP status code.
|
||||
* @param {string} fallbackKey i18n key to use for 5xx / unknown errors.
|
||||
* @param {string} [rateLimitKey] i18n key for 429 responses; defaults to 'form.rateLimited'.
|
||||
*/
|
||||
function extractApiError(body, status, fallbackKey, rateLimitKey) {
|
||||
if (status === 429) return t(rateLimitKey || 'form.rateLimited');
|
||||
var msg = body && (body.error || body.Error || body.title);
|
||||
return (status >= 400 && status < 500 && msg) ? msg : t(fallbackKey);
|
||||
}
|
||||
|
||||
function submitMSG(valid, msg, severity) {
|
||||
var tone = valid ? 'text-success' : ('text-' + (severity || 'danger'));
|
||||
$('#msgSubmit').removeClass().addClass('form-message ' + tone).text(msg);
|
||||
@@ -479,8 +494,7 @@
|
||||
if (resp && resp.ok === true) formSuccess();
|
||||
else submitMSG(false, t('form.captchaFailed'));
|
||||
}).fail(function (jqXHR) {
|
||||
var isRateLimited = jqXHR && jqXHR.status === 429;
|
||||
submitMSG(false, isRateLimited ? t('form.rateLimited') : t('form.failed'), isRateLimited ? 'warning' : 'danger');
|
||||
submitMSG(false, extractApiError(jqXHR.responseJSON, jqXHR.status, 'form.failed'), jqXHR.status === 429 ? 'warning' : 'danger');
|
||||
formError();
|
||||
}).always(function () {
|
||||
loader.hide();
|
||||
@@ -554,8 +568,11 @@
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (cvResponse.status === 429) throw new Error(t('cv.rateLimited'));
|
||||
if (!cvResponse.ok) throw new Error(t('cv.cvFailed'));
|
||||
if (!cvResponse.ok) {
|
||||
var cvErrBody = null;
|
||||
try { cvErrBody = await cvResponse.json(); } catch (_) {}
|
||||
throw new Error(extractApiError(cvErrBody, cvResponse.status, 'cv.cvFailed', 'cv.rateLimited'));
|
||||
}
|
||||
var cvData = await cvResponse.json();
|
||||
// Before calling match, obtain a fresh captcha token for the match action
|
||||
if (!(window.grecaptcha && reCaptchaSiteKey)) {
|
||||
@@ -586,8 +603,11 @@
|
||||
language: currentLang()
|
||||
})
|
||||
});
|
||||
if (matchResponse.status === 429) throw new Error(t('cv.rateLimited'));
|
||||
if (!matchResponse.ok) throw new Error(t('cv.matchFailed'));
|
||||
if (!matchResponse.ok) {
|
||||
var matchErrBody = null;
|
||||
try { matchErrBody = await matchResponse.json(); } catch (_) {}
|
||||
throw new Error(extractApiError(matchErrBody, matchResponse.status, 'cv.matchFailed', 'cv.rateLimited'));
|
||||
}
|
||||
var match = await matchResponse.json();
|
||||
renderMatchResult(match);
|
||||
$msg.removeClass().addClass('form-message text-success').text(t('cv.completed'));
|
||||
@@ -652,11 +672,8 @@
|
||||
setMsg('danger', 'form.captchaFailed');
|
||||
}
|
||||
}).fail(function (jqXHR) {
|
||||
if (jqXHR && jqXHR.status === 429) {
|
||||
setMsg('warning', 'form.rateLimited');
|
||||
} else {
|
||||
setMsg('danger', 'subscribe.failed');
|
||||
}
|
||||
$msg.removeClass().addClass('form-message text-' + (jqXHR.status === 429 ? 'warning' : 'danger'))
|
||||
.text(extractApiError(jqXHR.responseJSON, jqXHR.status, 'subscribe.failed'));
|
||||
}).always(function () {
|
||||
$loader.hide();
|
||||
$button.prop('disabled', false);
|
||||
|
||||
Reference in New Issue
Block a user