Skip to content

Commit 89e2330

Browse files
committed
fix: add Contents API fallback for CDN cache misses
1 parent 24c05bc commit 89e2330

1 file changed

Lines changed: 60 additions & 4 deletions

File tree

src/storage/repo/repo-client.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,18 +193,18 @@ export class RepoStorageBackend implements StorageBackend {
193193
}
194194

195195
/**
196-
* Fetch files via raw.githubusercontent.com (no API rate limits).
197-
* All fetches run in parallel - raw content endpoint has no rate limiting.
198-
* Adds cache-bust parameter to bypass CDN caching issues.
196+
* Fetch files via raw.githubusercontent.com with API fallback.
197+
* Uses raw endpoint first (no rate limits), falls back to Contents API
198+
* for files not found (CDN caching can cause stale 404s).
199199
*/
200200
private async fetchFilesViaRaw(
201201
paths: string[]
202202
): Promise<{ path: string; content: string | null }[]> {
203203
const branch = await this.getBranch();
204204
const baseUrl = `https://raw.githubusercontent.com/${this.owner}/${this.repo}/${branch}`;
205-
// Add cache-bust to avoid stale CDN responses after recent pushes
206205
const cacheBust = Date.now();
207206

207+
// First pass: try raw.githubusercontent.com (fast, no rate limits)
208208
const results = await Promise.all(
209209
paths.map(async (path) => {
210210
const content = await this.fetchRawFile(
@@ -214,9 +214,65 @@ export class RepoStorageBackend implements StorageBackend {
214214
})
215215
);
216216

217+
// Find files that returned null (might be CDN cache issue)
218+
const missingPaths = results.filter((r) => r.content === null).map((r) => r.path);
219+
220+
// Fallback: use Contents API for missing files (rate limited but fresh)
221+
if (missingPaths.length > 0) {
222+
this.logProgress(`Raw CDN missed ${String(missingPaths.length)} files, using API fallback`);
223+
const apiFetched = await this.fetchFilesViaContentsApi(missingPaths);
224+
// Merge API results back
225+
for (const result of results) {
226+
const apiContent = apiFetched[result.path];
227+
if (result.content === null && apiContent !== undefined) {
228+
result.content = apiContent;
229+
}
230+
}
231+
}
232+
217233
return results;
218234
}
219235

236+
/**
237+
* Fetch files via Contents API (fallback for CDN cache misses).
238+
* This is rate-limited but always returns fresh data.
239+
*/
240+
private async fetchFilesViaContentsApi(paths: string[]): Promise<Record<string, string | null>> {
241+
const results: Record<string, string | null> = {};
242+
243+
// Fetch in parallel but limit concurrency to avoid rate limits
244+
const batchSize = 10;
245+
for (let i = 0; i < paths.length; i += batchSize) {
246+
const batch = paths.slice(i, i + batchSize);
247+
const batchResults = await Promise.all(
248+
batch.map(async (path) => {
249+
const content = await this.fetchFileViaContentsApi(path);
250+
return { path, content };
251+
})
252+
);
253+
for (const { path, content } of batchResults) {
254+
results[path] = content;
255+
}
256+
}
257+
258+
return results;
259+
}
260+
261+
/** Fetch a single file via Contents API */
262+
private async fetchFileViaContentsApi(path: string): Promise<string | null> {
263+
try {
264+
const res = await this.fetchAllowNotFound(`/contents/${SYNC_DIR}/${path}`);
265+
if (!res?.ok) return null;
266+
267+
const data = (await res.json()) as { content?: string; encoding?: string };
268+
if (!data.content || data.encoding !== 'base64') return null;
269+
270+
return Buffer.from(data.content, 'base64').toString('utf-8');
271+
} catch {
272+
return null;
273+
}
274+
}
275+
220276
/** Fetch a single file from raw.githubusercontent.com */
221277
private async fetchRawFile(url: string): Promise<string | null> {
222278
try {

0 commit comments

Comments
 (0)