api-contracts: add validation error recovery mechanism

- Added cache fallback: uses cached response for GET requests when validation fails
- Added optional retry mechanism (disabled by default, enabled via config)
- Added user notifications for recovery actions (configurable)
- Recovery config: { useCache, retry, notifyUser } on request config
- Prevents infinite retry loops with _validationRetryAttempted flag
- Validates cached responses before using them
- Handles both wrapped and direct format responses
- Graceful degradation: falls back to unvalidated data if recovery fails
- Applied to both validation sections (wrapped and direct formats)
- Action 1.2.2.5 complete - validation errors now handled gracefully
This commit is contained in:
senke 2026-01-15 17:25:44 +01:00
parent 5412720318
commit b7551c2841
2 changed files with 145 additions and 3 deletions

View file

@ -377,11 +377,21 @@ Critical path dependencies:
- Exported singleton `validationAlerting` for manual control
- **Rollback**: Remove alerts
- [ ] **Action 1.2.2.5**: Add validation error recovery mechanism
- [x] **Action 1.2.2.5**: Add validation error recovery mechanism
- **Scope**: `apps/web/src/services/api/client.ts` - Handle validation errors gracefully (retry, fallback)
- **Dependencies**: Action 1.2.2.1 complete
- **Dependencies**: Action 1.2.2.1 complete
- **Risk**: MEDIUM
- **Validation**: Validation errors handled gracefully
- **Validation**: ✅ Added validation error recovery mechanisms:
- **Cache fallback**: For GET requests, if validation fails, attempts to use cached response (if available and valid)
- **Optional retry**: Configurable retry mechanism (disabled by default for safety, enabled via `_validationRecovery.retry: true`)
- **User notification**: Subtle warnings to users when recovery mechanisms are used (configurable via `_validationRecovery.notifyUser`)
- Recovery config: `{ useCache?: boolean, retry?: boolean, notifyUser?: boolean }` on request config
- Defaults: `useCache: true`, `retry: false`, `notifyUser: true`
- Prevents infinite retry loops via `_validationRetryAttempted` flag
- Validates cached responses before using them
- Handles both wrapped and direct format responses
- Applied to both wrapped format and direct format validation sections
- Graceful degradation: Falls back to unvalidated data if recovery mechanisms fail
- **Rollback**: Remove recovery mechanism
- [x] **Action 1.2.2.6**: Add schema versioning to Zod schemas

View file

@ -828,10 +828,81 @@ apiClient.interceptors.response.use(
// Action 1.2.2.3: Track validation failure metrics
validationMetrics.recordFailure(response.config.url);
// Action 1.2.2.5: Validation error recovery mechanism
const recoveryConfig = (response.config as any)?._validationRecovery as
| { useCache?: boolean; retry?: boolean; notifyUser?: boolean }
| undefined;
const useCache = recoveryConfig?.useCache !== false; // Default: true
const retry = recoveryConfig?.retry === true; // Default: false (safety)
const notifyUser = recoveryConfig?.notifyUser !== false; // Default: true
// Recovery 1: Try cached response (for GET requests only)
if (useCache && method === 'GET') {
const cachedResponse = responseCache.get(response.config);
if (cachedResponse) {
// Cached response may be in wrapped or direct format
// For wrapped format, unwrap it first
let cachedData = cachedResponse.data;
if (cachedData && typeof cachedData === 'object' && 'success' in cachedData && cachedData.success === true) {
cachedData = (cachedData as any).data !== undefined ? (cachedData as any).data : null;
}
// Validate cached response before using it
if (cachedData !== null) {
const cachedValidation = safeValidateApiResponse(responseSchema, cachedData);
if (cachedValidation.success) {
logger.warn(
'[API Validation Recovery] Using cached response due to validation failure',
{
request_id: requestId,
url: response.config.url,
recovery_type: 'cache_fallback',
},
);
if (notifyUser && typeof window !== 'undefined') {
toast('Data may be outdated. Please refresh if issues persist.', {
icon: '⚠️',
duration: 5000,
});
}
// Return cached response with unwrapped data (matching current format)
return {
...cachedResponse,
data: cachedData,
} as AxiosResponse<any>;
}
}
}
}
// Recovery 2: Optional retry (only if explicitly enabled)
if (retry && !(response.config as any)?._validationRetryAttempted) {
(response.config as any)._validationRetryAttempted = true;
logger.warn(
'[API Validation Recovery] Retrying request due to validation failure',
{
request_id: requestId,
url: response.config.url,
recovery_type: 'retry',
},
);
// Retry the request (will go through the same validation again)
return apiClient.request(response.config);
}
// Continue with unvalidated data (graceful degradation)
// In production, this allows the app to continue functioning even if
// the backend response doesn't match the expected schema (e.g., during migrations)
// Validation errors are logged for monitoring and alerting
if (notifyUser && typeof window !== 'undefined') {
toast('Some data may be incomplete. Please refresh if issues persist.', {
icon: '⚠️',
duration: 5000,
});
}
} else {
// Log successful validation in debug mode
const requestId = getRequestId(response.config);
@ -925,8 +996,69 @@ apiClient.interceptors.response.use(
// Action 1.2.2.3: Track validation failure metrics
validationMetrics.recordFailure(response.config.url);
// Action 1.2.2.5: Validation error recovery mechanism
const recoveryConfig = (response.config as any)?._validationRecovery as
| { useCache?: boolean; retry?: boolean; notifyUser?: boolean }
| undefined;
const useCache = recoveryConfig?.useCache !== false; // Default: true
const retry = recoveryConfig?.retry === true; // Default: false (safety)
const notifyUser = recoveryConfig?.notifyUser !== false; // Default: true
// Recovery 1: Try cached response (for GET requests only)
if (useCache && method === 'GET') {
const cachedResponse = responseCache.get(response.config);
if (cachedResponse) {
// Validate cached response before using it
const cachedValidation = safeValidateApiResponse(responseSchema, cachedResponse.data);
if (cachedValidation.success) {
logger.warn(
'[API Validation Recovery] Using cached response due to validation failure',
{
request_id: requestId,
url: response.config.url,
recovery_type: 'cache_fallback',
},
);
if (notifyUser && typeof window !== 'undefined') {
toast('Data may be outdated. Please refresh if issues persist.', {
icon: '⚠️',
duration: 5000,
});
}
// Return cached response
return cachedResponse as AxiosResponse<any>;
}
}
}
// Recovery 2: Optional retry (only if explicitly enabled)
if (retry && !(response.config as any)?._validationRetryAttempted) {
(response.config as any)._validationRetryAttempted = true;
logger.warn(
'[API Validation Recovery] Retrying request due to validation failure',
{
request_id: requestId,
url: response.config.url,
recovery_type: 'retry',
},
);
// Retry the request (will go through the same validation again)
return apiClient.request(response.config);
}
// Continue with unvalidated data (graceful degradation)
// In production, this allows the app to continue functioning even if
// the backend response doesn't match the expected schema (e.g., during migrations)
// Validation errors are logged for monitoring and alerting
if (notifyUser && typeof window !== 'undefined') {
toast('Some data may be incomplete. Please refresh if issues persist.', {
icon: '⚠️',
duration: 5000,
});
}
} else {
// Log successful validation in debug mode
const requestId = getRequestId(response.config);