392 lines
10 KiB
Markdown
392 lines
10 KiB
Markdown
|
|
# 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
|
||
|
|
|