# 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

