Skip to content

Return 400 for malformed affiliate offer JSON#471

Merged
ralyodio merged 1 commit into
profullstack:masterfrom
rissrice2105-agent:codex/affiliate-offers-invalid-json
Jun 14, 2026
Merged

Return 400 for malformed affiliate offer JSON#471
ralyodio merged 1 commit into
profullstack:masterfrom
rissrice2105-agent:codex/affiliate-offers-invalid-json

Conversation

@rissrice2105-agent

Copy link
Copy Markdown
Contributor

Fixes #470.

Changes

  • parse affiliate offer POST bodies safely
  • return 400 for malformed or non-object JSON before service/database work
  • add a regression test

Verification

  • corepack pnpm vitest run src/app/api/affiliates/offers/route.test.ts
  • corepack pnpm tsc --noEmit

@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

This 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.

  • Introduces readJsonObject, a small helper that wraps request.json() in a try/catch and rejects null, arrays, and primitives, returning null on any failure — the handler then immediately responds with { error: "Invalid JSON body" } and status 400.
  • A targeted regression test covers the malformed JSON case and asserts that neither Supabase nor the rate-limit path is reached.

Confidence Score: 4/5

Safe 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 body as unknown as OfferInput double cast introduced by the new typed return value of readJsonObject; it works because validateOfferInput has runtime type checks throughout, but it quietly bypasses the compiler on the call site.

No files require special attention; the cast in route.ts line 167 is noted but non-blocking.

Important Files Changed

Filename Overview
src/app/api/affiliates/offers/route.ts Adds readJsonObject helper to parse POST body safely, returning 400 for malformed or non-object JSON; introduces a double-cast (body as unknown as OfferInput) to satisfy the existing validateOfferInput signature
src/app/api/affiliates/offers/route.test.ts Adds a regression test for the malformed JSON 400 path; asserts correct status code, error message, and that no DB call (mockFrom) is made

Sequence Diagram

sequenceDiagram
    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 }
Loading

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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!

@rissrice2105-agent

Copy link
Copy Markdown
Contributor Author

CI is green for PR #471.

Verification:

  • corepack pnpm vitest run src/app/api/affiliates/offers/route.test.ts
  • corepack pnpm tsc --noEmit

uGig invoice evidence has been sent for this PR.

@ralyodio ralyodio merged commit 1aa19cb into profullstack:master Jun 14, 2026
6 checks passed
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.

POST /api/affiliates/offers returns 500 for malformed JSON

2 participants