Skip to content

Bug: Idempotency Key Not Checked Before Processing in order_service #10

@kushpatel2601

Description

@kushpatel2601

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.pycreate_order() function

Steps to Reproduce

  1. Start all services with docker compose up -d --build.
  2. Login and obtain a JWT token via POST /api/v1/login.
  3. Send POST /api/v1/orders with a valid body and an Idempotency-Key: test-key-123 header.
  4. Immediately send the exact same request again with the same Idempotency-Key: test-key-123.
  5. 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 HTTP 200.
  • 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:

  1. ✅ Idempotency key is read from the header.
  2. ❌ No early DB lookup is performed.
  3. ❌ User Service is called unnecessarily.
  4. ❌ Product Service is called unnecessarily.
  5. Stock is reserved again for all items in the order.
  6. ❌ DB INSERT fails with a UNIQUE KEY constraint violation.
  7. ❌ 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions