Skip to content

fix(api): answer bad requests to /api/grade with 4xx, not 500 - #31

Merged
lautaropaske merged 1 commit into
mainfrom
fix/grade-client-errors
Aug 27, 2026
Merged

lautaropaske merged 1 commit into
mainfrom
fix/grade-client-errors

Conversation

@lautaropaske

Copy link
Copy Markdown
Contributor

Found while diagnosing production error-log noise from the Vercel drain.

Problem

Every malformed request to /api/grade was logged as a server error and answered with a 500:

GET open.silver.dev/api/grade?name=2026  → 500
Error: MissingURL   at .next/server/pages/api/grade.js
UA: bingbot/2.0

Still reproducible on production right now:

curl -i "https://open.silver.dev/api/grade?name=2026"   → 500 {"error":"MissingURL"}
curl -i "https://open.silver.dev/api/grade"             → 500 {"error":"MissingURL"}

Two causes:

  1. src/pages/api/grade.ts threw Error("MissingURL") into its generic catch, which console.errors and returns 500. A client mistake reported as a server fault.
  2. The traffic comes from src/resume-checker/pages/index.tsx — the dropzone is a real <form action="/api/grade"> with a honeypot field name="name". Crawlers ignore method="POST" and issue a GET with the field names attached, which is exactly the ?name= shape in the drain. robots.ts (merged in fix(seo): sitemap, robots, canonicals and per-page titles/descriptions #28) stops the crawl traffic; this fixes the response itself.

Also on the 500 path, res.send({ error: e.message }) returned the raw error. The resume checker renders that string directly into a red badge (error-badge.tsx prints error.message with no mapping), so whatever pdf-parse or the model threw was being shown to end users. Same reasoning as c5179a0 for the take-home checker.

Change

Validate method and parameters before the try block:

request before after
GET ?name=2026 500 400 MissingURL
GET no params 500 400 MissingURL
GET ?url= (empty) 500 400 MissingURL
PUT 404 405 MethodNotAllowed
POST non-multipart 500 400 InvalidUploadRequest
internal failure 500 + raw message 500 GradingError

InvalidPDFException still answers 400, and the example-resume shortcut is untouched.

Test plan

  • tsc, next lint, next build pass
  • every row of the table above verified against next start
  • ?url=public/s_resume.pdf still returns 200 with the example grade
  • a URL serving non-PDF content still returns 400 InvalidPDFException
  • an unfetchable URL returns 500 GradingError instead of leaking fetch failed internals

Supersedes #12

#12 fixes the same status codes but has gone stale: it pins google("gemini-2.5-flash") via @ai-sdk/google with mode: "json", which would revert the AI Gateway routing merged in #18 for SIL-3430. This PR takes the same early-validation shape on top of current main without that regression. Suggest closing #12 in favour of this, or cherry-picking its contract tests on top.

Not addressed here

Still open in the same file, deliberately out of scope:

  • SSRF — fetch(url) at grade.ts takes an arbitrary caller-supplied URL with no scheme/host allowlist, no timeout and no response-size cap, on a route with maxDuration: 300. The client-side https/.pdf guard is bypassed by calling the API directly.
  • Unbounded body buffering — bodyParser: false plus Buffer.concat over the whole stream, no size limit.

🤖 Generated with Claude Code

https://claude.ai/code/session_0143SXEp4gahUMp68Dshbc6y

`/api/grade` threw `Error("MissingURL")` into its generic catch, which
logged it with `console.error` and returned 500. Search crawlers hit this
constantly: the resume-checker dropzone is a real
`<form action="/api/grade">` carrying a honeypot field named `name`, and
crawlers ignore `method="POST"` and issue a GET with the field names — the
`?name=` shape visible in the log drain. Every one of those was a 500 in
the error logs and a broken endpoint as far as Bing was concerned.

Validate method and parameters before the try block so client mistakes
stay 4xx, and stop returning `e.message` on the 500 path — the resume
checker renders that string straight into a badge, so whatever the PDF
parser or the model threw was being shown to the user. Same reasoning as
c5179a0 for the take-home checker.

  GET  ?name=2026   500 -> 400 MissingURL
  GET  (no params)  500 -> 400 MissingURL
  GET  ?url=        500 -> 400 MissingURL
  PUT               404 -> 405 MethodNotAllowed
  POST non-multipart  -> 400 InvalidUploadRequest
  internal failure  500 raw message -> 500 GradingError

`InvalidPDFException` still answers 400 and the example-resume shortcut is
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143SXEp4gahUMp68Dshbc6y
@vercel

vercel Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
open-silver Ready Ready Preview Aug 25, 2026 6:11pm

Request Review

@lautaropaske
lautaropaske merged commit a8e92c1 into main Aug 27, 2026
2 checks passed
@lautaropaske
lautaropaske deleted the fix/grade-client-errors branch August 27, 2026 00:19

This branch was successfully deployed

1 active deployment
Preview — e74af7a0 Deployed Aug 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant