veza/RATE_LIMITING_COMMUNICATION_GUIDE.md

392 lines
10 KiB
Markdown
Raw Normal View History

# Rate Limiting Communication Guide
## INT-013: Add API rate limiting communication
**Date**: 2025-12-25
**Status**: Completed
## Overview
This guide documents how rate limiting is communicated between the Veza backend API and frontend clients. It covers response formats, headers, error handling, and best practices.
## Backend Rate Limiting
### Rate Limit Configuration
The Veza API implements multiple rate limiting strategies:
1. **IP-based rate limiting** (unauthenticated users)
- Default: 100 requests per minute
- Burst: 10 requests
2. **User-based rate limiting** (authenticated users)
- Default: 1000 requests per minute
- Burst: 100 requests
3. **Endpoint-specific rate limiting**
- Login attempts: Configurable (default: 5 attempts per 15 minutes)
- Upload endpoints: 10 uploads per hour per user
### Rate Limit Response Format
When a rate limit is exceeded, the backend returns:
**HTTP Status**: `429 Too Many Requests`
**Headers**:
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1703509200
Retry-After: 60
```
**Response Body** (Standardized APIResponse format):
```json
{
"success": false,
"error": {
"code": 429,
"message": "Rate limit exceeded. Please try again later.",
"details": [
{
"field": "rate_limit",
"message": "You have exceeded the rate limit of 100 requests per minute"
}
],
"retry_after": 60,
"limit": 100,
"remaining": 0,
"reset": 1703509200
}
}
```
### Response Fields
- **`code`**: Error code (429 for rate limiting)
- **`message`**: Human-readable error message
- **`details`**: Array of validation/error details
- **`retry_after`**: Number of seconds to wait before retrying
- **`limit`**: Maximum number of requests allowed
- **`remaining`**: Number of requests remaining (0 when limit exceeded)
- **`reset`**: Unix timestamp when the rate limit resets
### Headers Explained
- **`X-RateLimit-Limit`**: Maximum number of requests allowed in the time window
- **`X-RateLimit-Remaining`**: Number of requests remaining in the current window
- **`X-RateLimit-Reset`**: Unix timestamp when the rate limit window resets
- **`Retry-After`**: Number of seconds to wait before retrying (RFC 7231)
## Frontend Rate Limit Handling
### Error Detection
The frontend detects rate limit errors by:
1. **HTTP Status Code**: `429`
2. **Response Format**: Standardized `APIResponse` with `success: false`
3. **Error Code**: `429` in error object
### Error Parsing
The frontend parses rate limit errors in `apiErrorHandler.ts`:
```typescript
if (status === 429) {
// Extract rate limit information from headers
const rateLimitLimit = headers['x-ratelimit-limit'];
const rateLimitRemaining = headers['x-ratelimit-remaining'];
const rateLimitReset = headers['x-ratelimit-reset'];
const retryAfter = headers['retry-after'] || data?.error?.retry_after || 60;
// Calculate time until reset
const resetTime = rateLimitReset
? new Date(rateLimitReset * 1000)
: undefined;
const secondsUntilReset = resetTime
? Math.max(0, Math.ceil((resetTime.getTime() - Date.now()) / 1000))
: retryAfter;
return {
code: 429,
message: data?.error?.message || 'Trop de requêtes. Veuillez patienter avant de réessayer.',
retry_after: secondsUntilReset,
rate_limit: {
limit: rateLimitLimit,
remaining: rateLimitRemaining,
reset: resetTime?.toISOString(),
},
};
}
```
### User Notification
When a rate limit error occurs:
1. **Toast Notification**: Shows error message with retry information
```typescript
toast.error(errorMessage, {
duration: 8000, // Longer duration for rate limit errors
});
```
2. **Error Message**: User-friendly message in French
```
"Trop de requêtes. Veuillez patienter quelques instants"
```
3. **Retry Information**: Includes time until reset
```
"Réessayez dans 60 secondes"
```
### Automatic Retry
The frontend implements automatic retry for rate limit errors:
1. **Retry Configuration**: Rate limit errors are included in retryable status codes
```typescript
retryableStatusCodes: [429, 500, 502, 503, 504]
```
2. **Exponential Backoff**: Retries use exponential backoff
```typescript
// For 429 errors, use retry_after if available
if (status === 429 && retryAfter) {
delay = retryAfter * 1000; // Use retry_after in seconds
}
```
3. **Retry Limits**: Maximum retries configured to prevent infinite loops
```typescript
maxRetries: 3
```
## Best Practices
### For Backend Developers
1. **Always Include Headers**: Include all rate limit headers in responses
```go
c.Header("X-RateLimit-Limit", strconv.Itoa(limit))
c.Header("X-RateLimit-Remaining", strconv.Itoa(remaining))
c.Header("X-RateLimit-Reset", strconv.FormatInt(resetTime, 10))
c.Header("Retry-After", strconv.Itoa(retryAfter))
```
2. **Use Standardized Format**: Use `APIResponse` format for consistency
```go
c.JSON(http.StatusTooManyRequests, gin.H{
"success": false,
"error": gin.H{
"code": 429,
"message": "Rate limit exceeded. Please try again later.",
"retry_after": retryAfter,
"limit": limit,
"remaining": 0,
"reset": resetTime,
},
})
```
3. **Calculate Accurate Reset Time**: Use actual window reset time, not fixed values
```go
resetTime := time.Now().Add(window).Unix()
```
4. **Provide Clear Messages**: Include helpful error messages
```go
"message": fmt.Sprintf("You have exceeded the rate limit of %d requests per %v", limit, window)
```
### For Frontend Developers
1. **Check Headers First**: Always check rate limit headers before parsing body
```typescript
const rateLimitLimit = headers['x-ratelimit-limit'];
const rateLimitRemaining = headers['x-ratelimit-remaining'];
```
2. **Use Retry-After**: Respect the `Retry-After` header for retry timing
```typescript
const retryAfter = headers['retry-after'] || 60;
```
3. **Show User-Friendly Messages**: Display clear messages to users
```typescript
"Trop de requêtes. Veuillez patienter quelques instants"
```
4. **Implement Exponential Backoff**: Use exponential backoff for retries
```typescript
delay = Math.min(delay * 2, maxDelay);
```
5. **Track Rate Limit State**: Store rate limit information for UI display
```typescript
rate_limit: {
limit: rateLimitLimit,
remaining: rateLimitRemaining,
reset: resetTime,
}
```
## Rate Limit Headers Usage
### Reading Headers in Frontend
```typescript
// Get rate limit information from response headers
const getRateLimitInfo = (response: AxiosResponse) => {
const headers = response.headers;
return {
limit: parseInt(headers['x-ratelimit-limit'] || '0', 10),
remaining: parseInt(headers['x-ratelimit-remaining'] || '0', 10),
reset: parseInt(headers['x-ratelimit-reset'] || '0', 10),
retryAfter: parseInt(headers['retry-after'] || '0', 10),
};
};
```
### Displaying Rate Limit Status
```typescript
// Show rate limit status in UI
const RateLimitIndicator = ({ rateLimit }) => {
if (!rateLimit) return null;
const resetTime = new Date(rateLimit.reset * 1000);
const timeUntilReset = Math.ceil((resetTime.getTime() - Date.now()) / 1000);
return (
<div className="rate-limit-indicator">
<span>Requests: {rateLimit.remaining} / {rateLimit.limit}</span>
{rateLimit.remaining === 0 && (
<span>Resets in {timeUntilReset} seconds</span>
)}
</div>
);
};
```
## Testing Rate Limiting
### Backend Tests
```go
func TestRateLimitMiddleware(t *testing.T) {
router := setupTestRouter()
// Make requests up to limit
for i := 0; i < 100; i++ {
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v1/tracks", nil)
router.ServeHTTP(w, req)
if i < 100 {
assert.Equal(t, http.StatusOK, w.Code)
} else {
assert.Equal(t, http.StatusTooManyRequests, w.Code)
assert.Equal(t, "0", w.Header().Get("X-RateLimit-Remaining"))
}
}
}
```
### Frontend Tests
```typescript
describe('Rate Limit Handling', () => {
it('should parse rate limit error correctly', () => {
const error = {
response: {
status: 429,
headers: {
'x-ratelimit-limit': '100',
'x-ratelimit-remaining': '0',
'x-ratelimit-reset': '1703509200',
'retry-after': '60',
},
data: {
success: false,
error: {
code: 429,
message: 'Rate limit exceeded',
retry_after: 60,
},
},
},
};
const apiError = parseApiError(error);
expect(apiError.code).toBe(429);
expect(apiError.retry_after).toBe(60);
expect(apiError.rate_limit.limit).toBe(100);
});
});
```
## Common Scenarios
### Scenario 1: User Exceeds Rate Limit
1. User makes 101 requests in 1 minute (limit: 100)
2. Backend returns `429` with rate limit headers
3. Frontend shows toast: "Trop de requêtes. Veuillez patienter quelques instants"
4. Frontend waits `retry_after` seconds before retrying
5. Request automatically retries after delay
### Scenario 2: Rate Limit Reset
1. User hits rate limit at 10:00:00
2. Backend sets `X-RateLimit-Reset: 1703509260` (10:01:00)
3. Frontend calculates: 60 seconds until reset
4. User sees: "Réessayez dans 60 secondes"
5. At 10:01:00, rate limit resets automatically
### Scenario 3: Different Limits for Authenticated Users
1. Unauthenticated user: 100 requests/minute
2. Authenticated user: 1000 requests/minute
3. Headers reflect appropriate limit
4. Frontend displays limit based on user status
## Troubleshooting
### Issue: Rate limit not working
**Check**:
- Rate limit middleware is applied
- Redis is running (if using Redis-based rate limiting)
- Headers are being set correctly
### Issue: Frontend not showing rate limit errors
**Check**:
- Error handler is checking for status 429
- Headers are being parsed correctly
- Toast notifications are enabled
### Issue: Retry not respecting retry_after
**Check**:
- `retry_after` is being extracted from headers
- Retry delay is using `retry_after` value
- Exponential backoff is not overriding `retry_after`
## References
- [RFC 7231 - Retry-After Header](https://tools.ietf.org/html/rfc7231#section-7.1.3)
- [HTTP 429 Status Code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429)
- `ERROR_RESPONSE_STANDARD.md` - Standard error response format
- `REQUEST_RESPONSE_VALIDATION_GUIDE.md` - Validation guide
---
**Last Updated**: 2025-12-25
**Maintained By**: Veza Backend Team