fix(httpApiV1): surface 400-class delete validation errors instead of opaque 500#2488
Conversation
|
@momothemage is attempting to deploy a commit to the Amantus Machina Team on Vercel. A member of the Team first needs to authorize it. |
|
Codex review: passed. Reviewed June 4, 2026, 3:30 AM ET / 07:30 UTC. Summary Reproducibility: yes. Source inspection shows current main maps wrapped package-name validation failures through the generic 500 path because it checks the raw message and only has a hard-coded Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Merge the narrow HTTP boundary fix if maintainers accept the whitelist-based exposure as an interim path toward typed Convex error codes. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main maps wrapped package-name validation failures through the generic 500 path because it checks the raw message and only has a hard-coded Is this the best way to solve the issue? Yes, if maintainers accept the interim whitelist. The patch fixes the shared HTTP mapper, keeps unknown failures generic, and avoids changing CLI or package-name semantics. AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against 909a47106e0f. Label changesLabel justifications:
Evidence reviewedWhat I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
@clawsweeper automerge |
|
🦞🔧 Source: I will update this PR branch, or open a safe credited replacement, if the repair worker finds a narrow CI fix. Automerge progress:
|
|
Merged via squash.
Thanks @momothemage! |
Why
DELETE /api/v1/packages/<name>(and the parallel skill / soul soft-delete endpoints) currently return an opaque500 Internal Server Errorwith no body context whenever the underlying mutation throws anything outside a tiny keyword whitelist.The case #2482 that surfaced this:
Under the hood:
packageName = "xrow/xrow-self-improvement".softDeletePackageInternalcallsnormalizePackageName, whose regex^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$accepts either a bare name or@scope/name. Barescope/name(no@) is rejected withConvexError("Package name must be lowercase and npm-safe (example: @scope/name or plugin-name)").softDeleteErrorToResponseonly knows four keywords (unauthorized,forbidden,not found,slug required); everything else is rewritten to literal"Internal Server Error"+ 500. The actual reason is dropped.That is a textbook silent-drop: a 4xx user-input error reported as 5xx with the diagnostic text discarded. CLI users have no way to recover without server logs.
What
convex/httpApiV1/shared.ts— rewritesoftDeleteErrorToResponse:cleanUserFacingErrorMessagefirst so[CONVEX]/ConvexError:/[Request ID:...]prefixes don't hide the real text from the keyword check.SOFT_DELETE_BAD_REQUEST_HINTSwhitelist for input-validation failures (slug required,package name required,package name must be,must be lowercase,npm-safe,reserved,version required). Matches now return 400 + cleaned message so the CLI / HTTP client sees the reason.500with the bare bodyInternal Server Error. The cleaned message is computed but intentionally not appended, so unexpected exceptions do not leak diagnostic detail across the public HTTP boundary.convex/httpApiV1.shared.test.ts— adds dedicated regression coverage for both the new 400 path (cleaned validation message) and the unknown-failure generic-500 boundary, so the security-boundary contract is pinned.No schema / route / CLI changes. The 401, 403, and 404 paths and
formatAuthzMessagefallbacks are unchanged.Behavioral diff (HTTP)
DELETE /packages/xrow/xrow-self-improvement500 Internal Server Error400 Package name must be lowercase and npm-safe (example: @scope/name or ...)Slug required400 Slug required(hard-coded body)400 Slug required(cleaned message, same status)Forbidden: hidden by moderation403 Forbidden: hidden by moderationSkill not found404 Skill not foundnew Error("boom")(truly unexpected)500 Internal Server Error500 Internal Server Error(unchanged)Tests
bunx oxfmt convex/httpApiV1/shared.ts convex/httpApiV1.shared.test.ts— clean.read_lintsonshared.ts— no findings.bun run ci:static+bun run ci:types-build+bun run ci:unit— all green on the squashed prep head before push.static,unit,e2e-http,types-build,packages,playwright-smoke,playwright-local-auth, allCodeQL Lightanalyses, andScan for Verified Secrets— allSUCCESS. Only the externalVercel – clawhubstatus isFAILURE(Vercel team authorization), which is not a required check.Risk
httpApiV1.handlers.test.tsalready had explicit cases for 401 / 403 / 404 / moderation 403 and a generic-500unknownassertion; that 500 assertion still pins the bareInternal Server Errorbody, matching the on-the-wire contract."Internal Server Error"acrossconvex/finds only this test, the newshared.tsconstant, and the new shared-helper regression — no other call site relies on the exact body string.Behavioral sweep
softDeleteErrorToResponseis itself fragile. A cleaner long-term shape would be to thread an explicitcodefield on the underlying ConvexErrors (e.g.INVALID_INPUT,NOT_FOUND) and route on that. Out of scope for this fix, which is intentionally minimal.Out of scope
normalizePackageNameitself is not changed. Whether barescope/nameshould be auto-prefixed with@is a UX question for the CLI / schema packages, not for this HTTP-edge fix.Behavior proof (after fix)
ClawSweeper P1 asked for real post-fix behavior, not just unit asserts. The end-to-end output below was captured locally against the real
softDeleteErrorToResponsedriven by realConvexErrors from realnormalizePackageName(...). No mocks on the code under test.1. End-to-end HTTP boundary — before vs after (final HEAD)
Driver script: .local/proof-handler.ts — calls real
normalizePackageName(badInput)to throw a realConvexError, hands it to realsoftDeleteErrorToResponse("package", err, headers), then prints the realResponse.statusandResponse.body.origin/main)DELETE /packages/xrow/xrow-self-improvement500 Internal Server Error400 Package name must be lowercase and npm-safe (example: @scope/name or plugin-name)500 Internal Server Error400 Package name requiredConvexError("Slug required")400 Slug required400 Slug required(unchanged)ConvexError("Forbidden: hidden by moderation")403 Forbidden: hidden by moderation403 Forbidden: hidden by moderation(unchanged)ConvexError("Package not found")404 Package not found404 Package not found(unchanged)new Error("boom")(truly unexpected)500 Internal Server Error500 Internal Server Error(unchanged on the wire)Raw transcript — interim capture (
45448721, pre-revert; see note above)Raw transcript — BEFORE (PR base,
origin/main)2. Vitest regression run (final HEAD)
convex/httpApiV1.shared.test.tsadds two pinned assertions on the final HEAD:"Internal Server Error".The
convex/httpApiV1.handlers.test.tsunknown500 assertion continues to pin the bare"Internal Server Error"body, so the on-the-wire 500 contract is unchanged.Repro
proof-handler.ts