fix(cache): preserve local data on remote 5xx errors
Only update cache when remote returns HTTP 200. On 5xx errors or timeouts, preserve existing local cache instead of overwriting with empty/error data.
This commit is contained in:
@@ -45,6 +45,7 @@ async function getCompleteCookies(userName: string, userPwd: string): Promise<st
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get activity details from API
|
* Get activity details from API
|
||||||
|
* Only returns data on HTTP 200. Returns null on any error (5xx, timeout, etc.)
|
||||||
*/
|
*/
|
||||||
async function getActivityDetailsRaw(
|
async function getActivityDetailsRaw(
|
||||||
activityId: string,
|
activityId: string,
|
||||||
@@ -73,6 +74,16 @@ async function getActivityDetailsRaw(
|
|||||||
// Add additional timeout safety
|
// Add additional timeout safety
|
||||||
maxRedirects: 5
|
maxRedirects: 5
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// CRITICAL: Only accept HTTP 200. Reject all other status codes including 5xx
|
||||||
|
if (response.status !== 200) {
|
||||||
|
logger.error(`Non-200 status ${response.status} for activity ${activityId}. NOT updating cache to preserve local data.`);
|
||||||
|
if (attempt === maxRetries - 1) {
|
||||||
|
logger.error(`All ${maxRetries} retries failed with non-200 status for activity ${activityId}.`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(`Attempt ${attempt + 1}/${maxRetries} for activity ${activityId} - Received response status ${response.status}`);
|
logger.debug(`Attempt ${attempt + 1}/${maxRetries} for activity ${activityId} - Received response status ${response.status}`);
|
||||||
const outerData = JSON.parse(response.data);
|
const outerData = JSON.parse(response.data);
|
||||||
if (outerData && typeof outerData.d === 'string') {
|
if (outerData && typeof outerData.d === 'string') {
|
||||||
@@ -88,7 +99,7 @@ async function getActivityDetailsRaw(
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Only treat 401 (Unauthorized) and 403 (Forbidden) as authentication errors
|
// Only treat 401 (Unauthorized) and 403 (Forbidden) as authentication errors
|
||||||
// 404 (Not Found) is valid - activity doesn't exist
|
// 404 (Not Found) is valid - activity doesn't exist
|
||||||
// Other 4xx errors should not trigger re-authentication
|
// Other 4xx/5xx errors should not trigger re-authentication
|
||||||
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
|
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
|
||||||
logger.warn(`Authentication error (${error.response.status}) while fetching activity ${activityId}. Cookie may be invalid.`);
|
logger.warn(`Authentication error (${error.response.status}) while fetching activity ${activityId}. Cookie may be invalid.`);
|
||||||
throw new AuthenticationError(`Received ${error.response.status} for activity ${activityId}`, error.response.status);
|
throw new AuthenticationError(`Received ${error.response.status} for activity ${activityId}`, error.response.status);
|
||||||
@@ -97,6 +108,10 @@ async function getActivityDetailsRaw(
|
|||||||
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
logger.error(`Status: ${error.response.status}, Data (getActivityDetailsRaw): ${ String(error.response.data).slice(0,100)}...`);
|
logger.error(`Status: ${error.response.status}, Data (getActivityDetailsRaw): ${ String(error.response.data).slice(0,100)}...`);
|
||||||
|
// CRITICAL: 5xx errors should NOT update cache
|
||||||
|
if (error.response.status >= 500 && error.response.status < 600) {
|
||||||
|
logger.error(`Server error ${error.response.status} - preserving local cache, not updating.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (attempt === maxRetries - 1) {
|
if (attempt === maxRetries - 1) {
|
||||||
logger.error(`All ${maxRetries} retries failed for activity ${activityId}.`);
|
logger.error(`All ${maxRetries} retries failed for activity ${activityId}.`);
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ let skippedCount = 0;
|
|||||||
/**
|
/**
|
||||||
* Process and cache a single activity
|
* Process and cache a single activity
|
||||||
* @param activityId - The activity ID to process
|
* @param activityId - The activity ID to process
|
||||||
|
* @param forceUpdate - If true, update cache even on fetch failure (default: false)
|
||||||
* @returns The processed activity data
|
* @returns The processed activity data
|
||||||
*/
|
*/
|
||||||
async function processAndCacheActivity(activityId: string): Promise<ActivityData> {
|
async function processAndCacheActivity(activityId: string, forceUpdate: boolean = false): Promise<ActivityData> {
|
||||||
logger.debug(`Processing activity ID: ${activityId}`);
|
logger.debug(`Processing activity ID: ${activityId}`);
|
||||||
try {
|
try {
|
||||||
if (!USERNAME || !PASSWORD) {
|
if (!USERNAME || !PASSWORD) {
|
||||||
@@ -92,11 +93,21 @@ async function processAndCacheActivity(activityId: string): Promise<ActivityData
|
|||||||
let structuredActivity: ActivityData;
|
let structuredActivity: ActivityData;
|
||||||
|
|
||||||
if (!activityJson) {
|
if (!activityJson) {
|
||||||
logger.info(`No data found for activity ID ${activityId} from engage API. Caching as empty.`);
|
// CRITICAL: Only cache empty data if forceUpdate is true
|
||||||
|
// This prevents 5xx errors from overwriting valid local data
|
||||||
|
if (forceUpdate) {
|
||||||
|
logger.info(`No data found for activity ID ${activityId} from engage API. Force updating cache.`);
|
||||||
structuredActivity = {
|
structuredActivity = {
|
||||||
lastCheck: new Date().toISOString(),
|
lastCheck: new Date().toISOString(),
|
||||||
source: 'api-fetch-empty'
|
source: 'api-fetch-empty'
|
||||||
};
|
};
|
||||||
|
await setActivityData(activityId, structuredActivity);
|
||||||
|
return structuredActivity;
|
||||||
|
} else {
|
||||||
|
logger.warn(`No data for activity ${activityId}. Preserving existing cache - NOT updating.`);
|
||||||
|
const existingData = await getActivityData(activityId);
|
||||||
|
return existingData || { lastCheck: new Date().toISOString(), source: 'cache-preserved' };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
structuredActivity = await structActivityData(activityJson);
|
structuredActivity = await structActivityData(activityJson);
|
||||||
if (structuredActivity && structuredActivity.photo &&
|
if (structuredActivity && structuredActivity.photo &&
|
||||||
@@ -124,12 +135,19 @@ async function processAndCacheActivity(activityId: string): Promise<ActivityData
|
|||||||
return structuredActivity;
|
return structuredActivity;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error processing activity ID ${activityId}:`, error);
|
logger.error(`Error processing activity ID ${activityId}:`, error);
|
||||||
|
// CRITICAL: On error, preserve existing cache instead of overwriting with error data
|
||||||
|
if (forceUpdate) {
|
||||||
const errorData: ActivityData = {
|
const errorData: ActivityData = {
|
||||||
lastCheck: new Date().toISOString(),
|
lastCheck: new Date().toISOString(),
|
||||||
error: "Failed to fetch or process"
|
error: "Failed to fetch or process"
|
||||||
};
|
};
|
||||||
await setActivityData(activityId, errorData);
|
await setActivityData(activityId, errorData);
|
||||||
return errorData;
|
return errorData;
|
||||||
|
} else {
|
||||||
|
logger.warn(`Error fetching activity ${activityId}. Preserving existing cache.`);
|
||||||
|
const existingData = await getActivityData(activityId);
|
||||||
|
return existingData || { lastCheck: new Date().toISOString(), error: (error as Error).message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user