Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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

142 changes: 108 additions & 34 deletions apps/nextjs-website/src/helpers/s3Metadata.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,128 @@ 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
);

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}`
// 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 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}`
);
}

// Read the response body as text
const fileContent = await response.text();

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

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

// Read the response body as text
const fileContent = await response.text();
return fileContent;
} catch (error) {
console.error('Error downloading file:', error);
return;
// 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 download file after ${RETRY_ATTEMPTS} attempts:`,
lastError
);
return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isntead of duplicate code for retry maybe we can have a shared function that handle retries used in both downloadFileAsText and fetchMetadataFromCDN

}

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}`
// 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 {
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();

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

return bodyContent as readonly JsonMetadata[];
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
console.error(
`Error fetching metadata from CDN (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);
}
}
const bodyContent = await response.json();
return bodyContent as readonly JsonMetadata[];
} catch (error) {
console.error('Error fetching metadata from CDN:', error);
return null;
}

console.error(
`Failed to fetch metadata from CDN after ${RETRY_ATTEMPTS} attempts:`,
lastError
);
return null;
}

const S3_GUIDES_METADATA_JSON_PATH =
Expand Down
Loading