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:
parent
5412720318
commit
b7551c2841
2 changed files with 145 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue