[codex] Prevent empty affiliate offer slugs#415
Conversation
Greptile SummaryThis PR fixes issue #414 by adding a
Confidence Score: 3/5The empty-slug regression is correctly closed, but the hardcoded constant 'offer' makes concurrent emoji-only offer creation reliably collide on the same slug before the non-atomic uniqueness check can intervene. Every non-slugifiable title now funnels to the exact same base slug. Two simultaneous requests both compute 'offer', both query the DB and find no existing row, and then both attempt to insert — one will receive a unique-constraint error surfaced as a 400 to the user. This race is rare for normal titles; it becomes reproducible here because all such titles converge on a single hardcoded word. src/app/api/affiliates/offers/route.ts — specifically the fallback value returned by slugify(). Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[POST /api/affiliates/offers] --> B[validateOfferInput]
B -->|invalid| C[400 Bad Request]
B -->|valid| D["slugify(input.title)"]
D --> E{slug empty?}
E -->|no| F[slug = derived slug]
E -->|yes emoji-only title| G[slug = 'offer']
F --> H[Check DB: slug exists?]
G --> H
H -->|no collision| I[INSERT with slug]
H -->|collision| J["slug = slug + '-' + random8"]
J --> I
I -->|success| K[201 Created]
I -->|error| L[400 DB Error]
Reviews (1): Last reviewed commit: "fix: prevent empty affiliate offer slugs" | Re-trigger Greptile |
| .replace(/[^a-z0-9]+/g, "-") | ||
| .replace(/^-|-$/g, "") | ||
| .slice(0, 80); | ||
| return slug || "offer"; |
There was a problem hiding this comment.
All titles that produce no slug-safe characters (emoji, pure punctuation like
"---", CJK-only strings pre-Unicode normalization, etc.) collapse to the same fallback "offer". Because the uniqueness check and the insert are not atomic, two concurrent requests that both generate "offer" will both pass the if (existing) guard and race to insert — one will hit a unique-constraint violation. For normal titles this race is rare since each user's title is different; here every non-slugifiable title funnels to the same word, so concurrent emoji-only offers will trigger it reliably. Adding a short random suffix to the fallback itself (before the collision check runs) makes all fallback slugs distinct by default and sidesteps the race entirely.
| return slug || "offer"; | |
| return slug || `offer-${Math.random().toString(36).slice(2, 8)}`; |
Summary
offeras the fallback when a valid title has no slug-safe charactersslug: ""Fixes #414.
Validation
npm.cmd run test:run -- src/app/api/affiliates/offers/route.test.tsnpm.cmd run type-check