From 9749118334bd6510c1af75ffdefbd5834568784c Mon Sep 17 00:00:00 2001 From: Nnamdi Ken C Ojibe Date: Thu, 11 Jun 2026 19:29:59 -0400 Subject: [PATCH] fix: report clear size-limit errors for oversized blueprints in webhooks Blueprints over 1MB bypass the size check entirely: the GitHub Contents API returns encoding "none" with empty content for such files, which decoded to an empty string, passed the 500KB check at 0KB, and surfaced as a generic "Failed to fetch blueprint content" error. Files between 500KB and 1MB hit the size check, but the error was swallowed by the catch block and also reported generically. Check the API-reported size field (populated even when content is not inlined) before decoding, and propagate a structured error so PR comments tell contributors the file is too large and to split it. Also adds the same 500KB check to the push webhook, which had none. Seen on weval-org/configs#27 (~1.6MB) and #28 (~1.14MB). Co-Authored-By: Claude Fable 5 --- src/app/api/webhooks/github-pr/route.ts | 25 ++++++++++++---------- src/app/api/webhooks/github-push/route.ts | 26 +++++++++++++++++------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/app/api/webhooks/github-pr/route.ts b/src/app/api/webhooks/github-pr/route.ts index 5f791c7d..c4146876 100644 --- a/src/app/api/webhooks/github-pr/route.ts +++ b/src/app/api/webhooks/github-pr/route.ts @@ -125,7 +125,7 @@ function parseBlueprintFiles(files: any[], prAuthor: string): { /** * Fetch blueprint content from GitHub */ -async function fetchBlueprintContent(octokit: Octokit, owner: string, repo: string, ref: string, path: string): Promise { +async function fetchBlueprintContent(octokit: Octokit, owner: string, repo: string, ref: string, path: string): Promise<{ content: string | null; error?: string }> { try { const response = await octokit.repos.getContent({ owner, @@ -135,20 +135,23 @@ async function fetchBlueprintContent(octokit: Octokit, owner: string, repo: stri }); if ('content' in response.data && response.data.type === 'file') { - const content = Buffer.from(response.data.content, 'base64').toString('utf-8'); - - // Check size limit - const sizeKB = Buffer.byteLength(content, 'utf-8') / 1024; + // Check size limit using the API-reported size. For files over 1MB the + // Contents API returns `encoding: "none"` with empty content, so the + // reported size is the only reliable measure. + const sizeKB = response.data.size / 1024; if (sizeKB > MAX_BLUEPRINT_SIZE_KB) { - throw new Error(`Blueprint exceeds size limit (${sizeKB.toFixed(1)}KB > ${MAX_BLUEPRINT_SIZE_KB}KB)`); + return { + content: null, + error: `Blueprint is ${sizeKB.toFixed(1)}KB, which exceeds the ${MAX_BLUEPRINT_SIZE_KB}KB limit. Please split it into smaller files.`, + }; } - return content; + return { content: Buffer.from(response.data.content, 'base64').toString('utf-8') }; } - return null; + return { content: null, error: 'Path did not resolve to a file' }; } catch (error: any) { console.error(`[GitHub Webhook] Failed to fetch content for ${path}:`, error.message); - return null; + return { content: null, error: 'Failed to fetch blueprint content' }; } } @@ -407,7 +410,7 @@ export async function POST(req: NextRequest) { for (const file of valid) { // Fetch content from PR head - const content = await fetchBlueprintContent( + const { content, error: fetchError } = await fetchBlueprintContent( octokit, pr.head.repo.owner.login, pr.head.repo.name, @@ -416,7 +419,7 @@ export async function POST(req: NextRequest) { ); if (!content) { - validationErrors.push({ filename: file.filename, error: 'Failed to fetch blueprint content' }); + validationErrors.push({ filename: file.filename, error: fetchError || 'Failed to fetch blueprint content' }); continue; } diff --git a/src/app/api/webhooks/github-push/route.ts b/src/app/api/webhooks/github-push/route.ts index 40c2cdef..4e39b8dc 100644 --- a/src/app/api/webhooks/github-push/route.ts +++ b/src/app/api/webhooks/github-push/route.ts @@ -11,6 +11,7 @@ const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const UPSTREAM_OWNER = 'weval-org'; const UPSTREAM_REPO = 'configs'; const MAIN_BRANCH = 'main'; +const MAX_BLUEPRINT_SIZE_KB = 500; // 500KB max, matches the PR webhook limit const s3Client = new S3Client({ region: process.env.APP_S3_REGION!, @@ -101,7 +102,7 @@ async function fetchBlueprintContent( repo: string, ref: string, path: string -): Promise { +): Promise<{ content: string | null; error?: string }> { try { const response = await octokit.repos.getContent({ owner, @@ -111,12 +112,23 @@ async function fetchBlueprintContent( }); if ('content' in response.data && response.data.type === 'file') { - return Buffer.from(response.data.content, 'base64').toString('utf-8'); + // Check size limit using the API-reported size. For files over 1MB the + // Contents API returns `encoding: "none"` with empty content, so the + // reported size is the only reliable measure. + const sizeKB = response.data.size / 1024; + if (sizeKB > MAX_BLUEPRINT_SIZE_KB) { + return { + content: null, + error: `Blueprint is ${sizeKB.toFixed(1)}KB, which exceeds the ${MAX_BLUEPRINT_SIZE_KB}KB limit`, + }; + } + + return { content: Buffer.from(response.data.content, 'base64').toString('utf-8') }; } - return null; + return { content: null, error: 'Path did not resolve to a file' }; } catch (error: any) { console.error(`[GitHub Push Webhook] Failed to fetch ${path}:`, error.message); - return null; + return { content: null, error: 'Failed to fetch blueprint content' }; } } @@ -308,7 +320,7 @@ export async function POST(req: NextRequest) { console.log(`[GitHub Push Webhook] Processing ${file.filename}...`); // Fetch content - const content = await fetchBlueprintContent( + const { content, error: fetchError } = await fetchBlueprintContent( octokit, UPSTREAM_OWNER, UPSTREAM_REPO, @@ -317,8 +329,8 @@ export async function POST(req: NextRequest) { ); if (!content) { - console.error(`[GitHub Push Webhook] Failed to fetch content for ${file.filename}`); - results.errors.push(`${file.filename}: Failed to fetch content`); + console.error(`[GitHub Push Webhook] Failed to fetch content for ${file.filename}: ${fetchError}`); + results.errors.push(`${file.filename}: ${fetchError || 'Failed to fetch content'}`); continue; }