Bug Description
The POST /api/doubts handler creates new tags via db.insert(tagsTable).values(tagsToInsert).returning() without any conflict handling. When two users simultaneously create doubts that introduce the same new tag name, both requests pass the "does tag exist?" check, both attempt to INSERT, and the unique constraint violation on normalizedName crashes one of the requests with an unhandled 500 error — silently discarding the user's doubt.
Steps to Reproduce
- Open two browser windows, both authenticated
- In the same classroom, send
POST /api/doubts from both windows simultaneously
- Both requests include a tag name that does not yet exist in the tags table (e.g., "Quantum Computing")
- Both requests pass the
existingClassroomTags check (tag doesn't exist yet)
- One request succeeds; the other fails with a 500 error due to unique constraint violation on
tagsTable.normalizedName
Alternatively, send two concurrent POST /api/doubts requests with the same new tag using a tool like curl in parallel:
curl -X POST ... -d '{"subject":"Physics","content":"test","tags":["QuantumComputing"]}' &
curl -X POST ... -d '{"subject":"Physics","content":"test","tags":["QuantumComputing"]}' &
Expected Behavior
The second request should gracefully handle the conflict by either:
- Using
onConflictDoNothing() and re-querying the existing tag (as done elsewhere in the codebase), or
- Wrapping the tag creation in a unique constraint-aware insert that returns the existing row on conflict
Actual Behavior
In src/app/api/doubts/route.ts (approximately lines 228-244):
if (tagsToInsert.length > 0) {
const insertedRows = await db.insert(tagsTable).values(tagsToInsert).returning();
savedTags.push(...insertedRows);
}
There is no onConflictDoNothing() or any conflict handling. The unique constraint on tagsTable.normalizedName will throw a Postgres error on the second concurrent insert.
Contrast with the proper pattern in src/app/api/doubts/action/[id]/route.ts (PATCH handler):
const [createdTag] = await tx.insert(tagsTable).values({...}).onConflictDoNothing().returning();
if (createdTag) { tagRecord = createdTag; } else { /* re-query */ }
The action route correctly handles the race condition with onConflictDoNothing() and a fallback re-query. The main POST route does not.
Environment
- OS: All
- Browser: All
- Node.js version: 18+
Additional Context
While concurrent tag creation is rare in practice, the impact is severe: the user's doubt creation fails entirely because the tag insert throws before the doubt is returned. The user loses their typed content (which is not recoverable from a 500 response). This is a correctness issue that contradicts the principle that concurrent identical mutations should be safe.
Suggested Fix
Add onConflictDoNothing() to the tag insert, then re-query for the existing tag when the insert returns no rows:
if (tagsToInsert.length > 0) {
const insertedRows = await db
.insert(tagsTable)
.values(tagsToInsert)
.onConflictDoNothing()
.returning();
if (insertedRows.length < tagsToInsert.length) {
const remainingNames = tagsToInsert
.filter(t => !insertedRows.some((r: any) => r.normalizedName === t.normalizedName))
.map(t => t.normalizedName);
if (remainingNames.length > 0) {
const existingTags = await db
.select()
.from(tagsTable)
.where(
and(
inArray(tagsTable.normalizedName, remainingNames),
classroomId ? eq(tagsTable.classroomId, classroomId) : isNull(tagsTable.classroomId),
),
);
insertedRows.push(...existingTags);
}
}
savedTags.push(...insertedRows);
}
GSSoC 2026
I would like to work on this issue under GSSoC 2026. Please assign it to me.
Bug Description
The
POST /api/doubtshandler creates new tags viadb.insert(tagsTable).values(tagsToInsert).returning()without any conflict handling. When two users simultaneously create doubts that introduce the same new tag name, both requests pass the "does tag exist?" check, both attempt to INSERT, and the unique constraint violation onnormalizedNamecrashes one of the requests with an unhandled 500 error — silently discarding the user's doubt.Steps to Reproduce
POST /api/doubtsfrom both windows simultaneouslyexistingClassroomTagscheck (tag doesn't exist yet)tagsTable.normalizedNameAlternatively, send two concurrent
POST /api/doubtsrequests with the same new tag using a tool likecurlin parallel:Expected Behavior
The second request should gracefully handle the conflict by either:
onConflictDoNothing()and re-querying the existing tag (as done elsewhere in the codebase), orActual Behavior
In
src/app/api/doubts/route.ts(approximately lines 228-244):There is no
onConflictDoNothing()or any conflict handling. The unique constraint ontagsTable.normalizedNamewill throw a Postgres error on the second concurrent insert.Contrast with the proper pattern in
src/app/api/doubts/action/[id]/route.ts(PATCH handler):The action route correctly handles the race condition with
onConflictDoNothing()and a fallback re-query. The main POST route does not.Environment
Additional Context
While concurrent tag creation is rare in practice, the impact is severe: the user's doubt creation fails entirely because the tag insert throws before the doubt is returned. The user loses their typed content (which is not recoverable from a 500 response). This is a correctness issue that contradicts the principle that concurrent identical mutations should be safe.
Suggested Fix
Add
onConflictDoNothing()to the tag insert, then re-query for the existing tag when the insert returns no rows:GSSoC 2026
I would like to work on this issue under GSSoC 2026. Please assign it to me.