Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/cuddly-zebras-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"nextjs-website": patch
---

- Added configurable retry logic to `downloadFileAsText()` and `fetchMetadataFromCDN()` functions
- Implemented exponential backoff with configurable retry attempts and delay timing
- Added environment variables `CDN_RETRY_ATTEMPTS` (default: 3) and `CDN_RETRY_DELAY_MS` (default: 5000ms)
- Improved error handling and logging for CDN fetch failures
- Fixes issues where guide pages would show 404 errors when CloudFront cache was not warmed up

136 changes: 97 additions & 39 deletions apps/nextjs-website/src/helpers/s3Metadata.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,112 @@ export interface JsonMetadata {
readonly lastModified?: string;
}

const delay = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));

// Retry configuration - configurable via environment variables
const RETRY_ATTEMPTS = parseInt(process.env.CDN_RETRY_ATTEMPTS || '3', 10);
const INITIAL_RETRY_DELAY_MS = parseInt(
process.env.CDN_RETRY_DELAY_MS || '5000',
10
);

async function withRetries<T>(
operation: () => Promise<T>,
operationName: string,
fallbackValue: T
): Promise<T> {
// eslint-disable-next-line functional/no-let
let lastError: Error | null = null;

// eslint-disable-next-line functional/no-loop-statements
for (let attempt = 1; attempt <= RETRY_ATTEMPTS; attempt++) {
// eslint-disable-next-line functional/no-try-statements
try {
const result = await operation();

// Log successful retry if this wasn't the first attempt
if (attempt > 1) {
console.log(
`Successfully completed ${operationName} on attempt ${attempt}`
);
}

return result;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
console.error(
`Error during ${operationName} (attempt ${attempt}/${RETRY_ATTEMPTS}):`,
error
);

// If this isn't the last attempt, wait before retrying
if (attempt < RETRY_ATTEMPTS) {
const delayMs = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1); // Exponential backoff
console.log(`Retrying in ${delayMs}ms...`);
await delay(delayMs);
}
}
}

console.error(
`Failed to complete ${operationName} after ${RETRY_ATTEMPTS} attempts:`,
lastError
);
return fallbackValue;
}

export async function downloadFileAsText(
path: string
): Promise<string | undefined> {
// eslint-disable-next-line functional/no-try-statements
try {
const url = `${staticContentsUrl}/${path}`;
const response = await fetch(url);

if (!response.ok) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error(
`Failed to download file from ${url}: ${response.statusText}`
);
}
return withRetries(
async () => {
const url = `${staticContentsUrl}/${path}`;
const response = await fetch(url);

// Read the response body as text
const fileContent = await response.text();
return fileContent;
} catch (error) {
console.error('Error downloading file:', error);
return;
}
if (!response.ok) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error(
`Failed to download file from ${url}: ${response.statusText}`
);
}

// Read the response body as text
return await response.text();
},
`file download from ${path}`,
undefined
);
}

export async function fetchMetadataFromCDN(
path: string
): Promise<readonly JsonMetadata[] | null> {
// eslint-disable-next-line functional/no-try-statements
try {
if (!staticContentsUrl) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error(
'STATIC_CONTENTS_URL is not defined in the environment variables.'
);
}
const response = await fetch(`${staticContentsUrl}/${path}`);
if (!response.ok) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error(
`Failed to fetch metadata from ${staticContentsUrl}/${path}: ${response.statusText}`
);
}
const bodyContent = await response.json();
return bodyContent as readonly JsonMetadata[];
} catch (error) {
console.error('Error fetching metadata from CDN:', error);
return null;
}
return withRetries(
async () => {
if (!staticContentsUrl) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error(
'STATIC_CONTENTS_URL is not defined in the environment variables.'
);
}

const url = `${staticContentsUrl}/${path}`;
const response = await fetch(url);

if (!response.ok) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error(
`Failed to fetch metadata from ${url}: ${response.statusText}`
);
}

const bodyContent = await response.json();
return bodyContent as readonly JsonMetadata[];
},
`metadata fetch from ${path}`,
null
);
}

const S3_GUIDES_METADATA_JSON_PATH =
Expand Down
Loading