Skip to content

Commit 5f91323

Browse files
authored
fix(api): prevent MongoDB duplicate key failures during concurrent upserts (JhaSourav07#550)
## Description Fixes JhaSourav07#541 This PR resolves a MongoDB concurrency race condition inside `/api/track-user` that could trigger `E11000 duplicate key` exceptions during simultaneous requests for the same username. Previously, concurrent requests executing: findOneAndUpdate(..., { upsert: true }) could race against the unique `username` index. Under high concurrency: - multiple requests detected the document as missing simultaneously - multiple insert attempts were triggered - MongoDB threw `E11000 duplicate key` errors - the API incorrectly returned `500 Internal Server Error` This PR introduces safe duplicate-key race handling and treats these collisions as successful no-op operations. --- ## Changes Implemented ### Graceful Duplicate-Key Handling Wrapped the upsert operation inside a dedicated `try/catch` block and safely handled MongoDB duplicate-key race conditions: if ( upsertError && typeof upsertError === 'object' && 'code' in upsertError && upsertError.code === 11000 ) { return NextResponse.json({ success: true }); } This ensures: - concurrent duplicate inserts no longer fail - the endpoint remains idempotent - safe race conditions do not return HTTP 500 ### Added Regression Test Coverage Added a dedicated concurrency regression test that: - simulates `E11000` duplicate-key collisions - verifies `200 OK` responses - validates successful API behavior - ensures no unnecessary error logging occurs --- ## Files Changed - `app/api/track-user/route.ts` - `app/api/track-user/route.test.ts` --- ## Root Cause The route previously treated MongoDB duplicate-key collisions as fatal server errors. However, under concurrent upsert operations: - one request may successfully insert the username - a second simultaneous request may fail the unique index check - MongoDB throws `E11000 duplicate key error` This is an expected concurrency race and should be treated as a safe no-op instead of an internal server failure. --- ## Validation Successfully verified with: - `npm run test` - `npm run lint` - `npm run build` ### Results - 22/22 test suites passed - 276/276 tests passed - Production build compiled successfully --- ## Concurrency Validation Verified successfully using: - simultaneous identical username submissions - rapid repeated POST requests - mocked MongoDB duplicate-key collisions Confirmed: - no HTTP 500 responses occur - duplicate races resolve safely - API behavior remains stable and idempotent --- ## Impact This PR fixes: - MongoDB `E11000` concurrency crashes - unnecessary API failures - duplicate insert race instability - noisy database exception behavior while preserving: - existing API behavior - response structure - TypeScript safety - backward compatibility --- ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) --- ## Visual Preview N/A — backend concurrency reliability fix --- ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally. - [x] I have run `npm run lint` locally and resolved all errors. - [x] My commits follow the Conventional Commits format. - [x] I have made sure that I have only one commit in this PR. - [x] All tests and production builds pass successfully.
2 parents 335dadd + a5fee0f commit 5f91323

2 files changed

Lines changed: 78 additions & 9 deletions

File tree

app/api/track-user/route.test.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ vi.mock('@/lib/mongodb', () => ({
1010

1111
vi.mock('@/models/User', () => ({
1212
User: {
13-
findOneAndUpdate: vi.fn(),
13+
updateOne: vi.fn(),
1414
},
1515
}));
1616

@@ -99,10 +99,10 @@ describe('POST /api/track-user', () => {
9999
expect(dbConnect).toHaveBeenCalled();
100100

101101
// Trims and lowercases
102-
expect(User.findOneAndUpdate).toHaveBeenCalledWith(
102+
expect(User.updateOne).toHaveBeenCalledWith(
103103
{ username: 'octocat' },
104104
{ $setOnInsert: { username: 'octocat' } },
105-
{ upsert: true, new: true }
105+
{ upsert: true }
106106
);
107107

108108
expect(response.status).toBe(200);
@@ -124,5 +124,50 @@ describe('POST /api/track-user', () => {
124124

125125
consoleErrorSpy.mockRestore();
126126
});
127+
128+
it('gracefully handles concurrent duplicate key (code 11000) race conditions', async () => {
129+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
130+
131+
const mongoError = new Error('E11000 duplicate key error collection: username') as Error & {
132+
code?: number;
133+
keyPattern?: Record<string, number>;
134+
};
135+
mongoError.code = 11000;
136+
mongoError.keyPattern = { username: 1 };
137+
vi.mocked(User.updateOne).mockRejectedValueOnce(mongoError);
138+
139+
const response = await POST(makeRequest({ username: 'octocat' }));
140+
141+
expect(response.status).toBe(200);
142+
const data = await response.json();
143+
expect(data.success).toBe(true);
144+
expect(consoleErrorSpy).not.toHaveBeenCalled();
145+
146+
consoleErrorSpy.mockRestore();
147+
});
148+
149+
it('rethrows duplicate key (code 11000) error if it is not related to username', async () => {
150+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
151+
152+
const mongoError = new Error(
153+
'E11000 duplicate key error collection: other_field'
154+
) as Error & {
155+
code?: number;
156+
keyPattern?: Record<string, number>;
157+
};
158+
mongoError.code = 11000;
159+
mongoError.keyPattern = { other_field: 1 };
160+
vi.mocked(User.updateOne).mockRejectedValueOnce(mongoError);
161+
162+
const response = await POST(makeRequest({ username: 'octocat' }));
163+
164+
expect(response.status).toBe(500);
165+
const data = await response.json();
166+
expect(data.success).toBe(false);
167+
expect(data.error).toBe('Internal server error');
168+
expect(consoleErrorSpy).toHaveBeenCalled();
169+
170+
consoleErrorSpy.mockRestore();
171+
});
127172
});
128173
});

app/api/track-user/route.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,36 @@ export async function POST(req: Request) {
4646
// Connect to database
4747
await dbConnect();
4848

49-
// Upsert the user: create if doesn't exist, do nothing if exists
50-
await User.findOneAndUpdate(
51-
{ username: trimmedUsername },
52-
{ $setOnInsert: { username: trimmedUsername } },
53-
{ upsert: true, new: true }
54-
);
49+
try {
50+
// Upsert the user: create if doesn't exist, do nothing if exists
51+
await User.updateOne(
52+
{ username: trimmedUsername },
53+
{ $setOnInsert: { username: trimmedUsername } },
54+
{ upsert: true }
55+
);
56+
} catch (upsertError) {
57+
// Gracefully handle MongoDB E11000 duplicate key race conditions under high concurrency.
58+
// Concurrent upserts for the same username can race on the unique index, causing
59+
// MongoDB to throw a duplicate key error (code 11000) for one of the requests.
60+
// We can safely treat this as a successful no-op because another request already created it.
61+
if (
62+
upsertError &&
63+
typeof upsertError === 'object' &&
64+
'code' in upsertError &&
65+
upsertError.code === 11000
66+
) {
67+
const err = upsertError as Record<string, unknown>;
68+
const isUsernameConflict =
69+
(err.keyPattern && typeof err.keyPattern === 'object' && 'username' in err.keyPattern) ||
70+
(err.keyValue && typeof err.keyValue === 'object' && 'username' in err.keyValue) ||
71+
(typeof err.message === 'string' && err.message.includes('username'));
72+
73+
if (isUsernameConflict) {
74+
return NextResponse.json({ success: true });
75+
}
76+
}
77+
throw upsertError;
78+
}
5579

5680
return NextResponse.json({ success: true });
5781
} catch (error) {

0 commit comments

Comments
 (0)