Return 400 for malformed affiliate offer JSON#471
Conversation
Greptile SummaryThis PR adds safe JSON body parsing to the affiliate offers POST endpoint, returning a 400 before any service or database work when the body is malformed or not a plain object.
Confidence Score: 4/5Safe to merge — the guard is narrow in scope, the happy path is unchanged, and the regression test validates the new early-exit behaviour. The implementation is correct and well-tested. The only rough edge is the No files require special attention; the cast in Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant POST Handler
participant readJsonObject
participant checkRateLimit
participant validateOfferInput
participant Supabase
Client->>POST Handler: POST /api/affiliates/offers
POST Handler->>POST Handler: getAuthContext()
alt Not authenticated
POST Handler-->>Client: 401 Unauthorized
end
POST Handler->>checkRateLimit: check write rate limit
alt Rate limit exceeded
POST Handler-->>Client: 429 Too Many Requests
end
POST Handler->>readJsonObject: request.json()
alt Malformed JSON or non-object
readJsonObject-->>POST Handler: null
POST Handler-->>Client: 400 Invalid JSON body
end
readJsonObject-->>POST Handler: Record<string, unknown>
POST Handler->>validateOfferInput: validate + sanitize
alt Validation errors
POST Handler-->>Client: 400 validation error
end
POST Handler->>Supabase: check slug uniqueness
POST Handler->>Supabase: insert affiliate offer
POST Handler-->>Client: 201 { offer }
Reviews (1): Last reviewed commit: "Handle malformed affiliate offer JSON" | Re-trigger Greptile |
| if (!body) { | ||
| return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); | ||
| } | ||
| const validation = validateOfferInput(body as unknown as OfferInput); |
There was a problem hiding this comment.
The double cast
body as unknown as OfferInput is necessary because readJsonObject returns Record<string, unknown>, which TypeScript won't accept for OfferInput's required fields. Since validateOfferInput already does full runtime type-narrowing, the cast is safe in practice — but it silently bypasses the compiler. Accepting Record<string, unknown> in validateOfferInput directly (or returning OfferInput | null from a narrowing helper) would keep TypeScript honest here.
| const validation = validateOfferInput(body as unknown as OfferInput); | |
| const validation = validateOfferInput(body as OfferInput); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
CI is green for PR #471. Verification:
uGig invoice evidence has been sent for this PR. |
Fixes #470.
Changes
Verification
corepack pnpm vitest run src/app/api/affiliates/offers/route.test.tscorepack pnpm tsc --noEmit