Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions backend/API_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,47 @@ RATE_LIMIT_LOCKOUT_SECONDS=900

`/health` and `/metrics` are exempt through `@SkipRateLimit()`.

---

## πŸ” Idempotency Keys

Mutating endpoints that create a resource (currently `POST /gigs` and `POST /escrows`) accept an
optional `Idempotency-Key` header so retries β€” e.g. after a client timeout β€” don't create
duplicate resources.

### Client usage

```bash
curl -X POST https://api.example.com/escrows \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "depositor": "G...", "beneficiary": "G...", "amountXLM": "100" }'
```

- Generate a fresh, unique key (a UUID is recommended) **per logical operation**, not per HTTP
attempt β€” reuse the same key when retrying the same request.
- The key is scoped to the specific endpoint (method + route), so the same key value can safely
be reused across different endpoints (e.g. once for `POST /gigs` and separately for
`POST /escrows`) without colliding.

### Behavior

| Situation | Response |
|---|---|
| No `Idempotency-Key` header | Request is processed normally; not cached. |
| First request with a given key | Request is processed; the response is cached. |
| Retry with the same key **and the same body** | The original cached response is replayed (same status code and body) β€” the handler does not run again. |
| Retry with the same key **and a different body** | `422 Unprocessable Entity` β€” the key has already been used for a different payload. |
| Concurrent request with the same key while the first is still in flight | `409 Conflict` β€” a request with this key is already being processed; wait and retry rather than assuming failure. |

Cached responses are stored in Redis for `IDEMPOTENCY_KEY_TTL_SECONDS` (default 24h). Keys are
claimed atomically (`SET NX`), so concurrent duplicate requests cannot both create a resource. If
Redis is unavailable, idempotency protection is skipped and requests are processed normally
(fail-open) rather than blocking traffic.

Response bodies are cached in full, so avoid decorating `@Idempotent()` onto endpoints that return
very large or streamed payloads.

### Using Authentication in Swagger UI

1. Get your challenge and sign it
Expand Down Expand Up @@ -334,6 +375,7 @@ REDIS_URL=redis://localhost:6379
RATE_LIMIT_ABUSE_WINDOW_SECONDS=300
RATE_LIMIT_ABUSE_THRESHOLD=5
RATE_LIMIT_LOCKOUT_SECONDS=900
IDEMPOTENCY_KEY_TTL_SECONDS=86400
STELLAR_NETWORK=TESTNET
STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { StellarModule } from './stellar/stellar.module';
import { SentryModule } from './sentry/sentry.module';
import { RedisModule } from './common/redis/redis.module';
import { RateLimitModule } from './common/rate-limit/rate-limit.module';
import { IdempotencyModule } from './common/idempotency/idempotency.module';
import { UserProfileModule } from './user-profile/user-profile.module';
import { EventIngestionModule } from './event-ingestion/event-ingestion.module';
import { DisputeModule } from './dispute/dispute.module';
Expand All @@ -21,6 +22,7 @@ import { ReputationModule } from './reputation/reputation.module';
SentryModule,
RedisModule,
RateLimitModule,
IdempotencyModule,
AuthModule,
UserProfileModule,
EscrowModule,
Expand Down
11 changes: 11 additions & 0 deletions backend/src/common/idempotency/idempotency-key.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SetMetadata } from '@nestjs/common';

export const IDEMPOTENT_KEY = 'idempotent';

/**
* Marks a controller method as idempotent. When present, the
* IdempotencyKeyInterceptor will cache the handler's response keyed by
* (endpoint, Idempotency-Key header) and replay it on retries with
* the same key.
*/
export const Idempotent = () => SetMetadata(IDEMPOTENT_KEY, true);
Loading
Loading