-
-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Bug: Idempotency Key Not Checked Before Processing in order_service
Summary
The POST /api/v1/orders endpoint accepts an Idempotency-Key header to prevent duplicate order creation. However, the key is only enforced at the database INSERT level via a UNIQUE KEY constraint — after all expensive and side-effectful operations have already been executed. This means duplicate requests will call the User Service, call the Product Service, and reserve stock before ultimately failing at the DB insert, leaving the inventory in a corrupted state.
Affected File
order_service/app.py→create_order()function
Steps to Reproduce
- Start all services with
docker compose up -d --build. - Login and obtain a JWT token via
POST /api/v1/login. - Send
POST /api/v1/orderswith a valid body and anIdempotency-Key: test-key-123header. - Immediately send the exact same request again with the same
Idempotency-Key: test-key-123. - Observe that on the second request, the system still calls the Product Service and reserves stock again before returning an error.
Expected Behavior
If an Idempotency-Key is provided and a matching order already exists in the database, the endpoint should:
- Return the original order's response (
id+status) immediately with HTTP200. - Skip all external service calls — no User Service call, no Product Service call, no stock reservation.
- Be a pure no-op with zero side effects.
Actual Behavior
The duplicate request goes through the full execution pipeline:
- ✅ Idempotency key is read from the header.
- ❌ No early DB lookup is performed.
- ❌ User Service is called unnecessarily.
- ❌ Product Service is called unnecessarily.
- ❌ Stock is reserved again for all items in the order.
- ❌ DB
INSERTfails with aUNIQUE KEYconstraint violation. - ❌ Stock that was just reserved is released (rollback), but this adds extra load and a race condition window.
Root Cause
In order_service/app.py, inside create_order(), the Idempotency-Key is read early but never looked up in the database until the INSERT statement:
# Line 105 — key is read idmp_key = request.headers.get('Idempotency-Key')Lines 111–210 — full business logic runs regardless of whether key is duplicate:
user validation → address validation → product validation → stock reservation → DB insert
The UNIQUE KEY constraint on the idempotency_key column only prevents the duplicate record from being saved — it does not prevent the side effects that already happened before the insert.
Fix
Add an early database lookup for the Idempotency-Key immediately after reading it, before any external service calls:
idmp_key = request.headers.get('Idempotency-Key')Check idempotency key FIRST — before any external service calls
if idmp_key:
conn = get_db_connection()
if not conn:
return jsonify({'error': 'Database connection failed'}), 500
try:
cur = conn.cursor(dictionary=True)
cur.execute(
"SELECT id, status FROM orders WHERE idempotency_key = %s LIMIT 1",
(idmp_key,)
)
existing = cur.fetchone()
finally:
conn.close()
if existing:
# Return the original order response — idempotent, no side effects
return jsonify({'id': existing['id'], 'status': existing['status']}), 200
Impact
| Scenario | Before Fix | After Fix |
|---|---|---|
| Duplicate request with same key | Reserves stock again, then fails | Returns cached response immediately ✅ |
| Network retry by client | May cause double stock deduction | Safely returns original order ✅ |
| First-time request | Works normally | Works normally ✅ |
| Request without key | Works normally | Works normally ✅ |
Severity: 🔴 Critical — In a production environment with retrying clients or unreliable networks, this bug can cause inventory to be incorrectly decremented on every duplicate request, leading to stock going negative or orders being unfulfillable.
Environment
- Service:
order_service - Language: Python 3 / Flask
- Database: MySQL 8.0
- Relevant endpoint:
POST /api/v1/orders