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
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,20 @@ MYSQL_CONN_MAX_IDLE_TIME=1m
SERVER_HOST=localhost
SERVER_PORT=8080

# Rate Limiting Configuration
RATE_LIMIT_ENABLED=true
RATE_LIMIT_REQUESTS_PER_MINUTE=100
RATE_LIMIT_BURST_SIZE=20
RATE_LIMIT_WINDOW_SIZE=1m

# Environment
ENV=development

# Notes:
# - Use 127.0.0.1 instead of external IPs for DB_HOST to avoid connection reset issues
# - Connection pool settings help manage MySQL connections efficiently
# - MYSQL_CONN_MAX_LIFETIME should be less than MySQL's wait_timeout (default 8 hours)
# - MYSQL_CONN_MAX_IDLE_TIME closes idle connections to prevent reset issues
# - MYSQL_CONN_MAX_IDLE_TIME closes idle connections to prevent reset issues
# - Rate limiting protects against abuse: 100 req/min per IP by default
# - RATE_LIMIT_WINDOW_SIZE accepts Go duration format (1m, 30s, 2h, etc.)
# - Set RATE_LIMIT_ENABLED=false to disable rate limiting (not recommended for production)
90 changes: 90 additions & 0 deletions RATE_LIMITING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Rate Limiting

This API implements rate limiting to ensure fair usage and protect against abuse. The rate limiter uses a sliding window algorithm to track requests per client IP address.

## Configuration

Rate limiting can be configured using environment variables:

| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `RATE_LIMIT_ENABLED` | `true` | Enable or disable rate limiting |
| `RATE_LIMIT_REQUESTS_PER_MINUTE` | `100` | Maximum requests per minute per IP |
| `RATE_LIMIT_BURST_SIZE` | `20` | Burst size for initial requests |
| `RATE_LIMIT_WINDOW_SIZE` | `1m` | Time window for rate limiting |

## Response Headers

All API responses include the following rate limiting headers:

- `X-RateLimit-Limit`: The maximum number of requests allowed in the current window
- `X-RateLimit-Remaining`: The number of requests remaining in the current window
- `X-RateLimit-Reset`: Unix timestamp when the rate limit window resets (only on 429 responses)
- `Retry-After`: Number of seconds to wait before making another request (only on 429 responses)

## Rate Limit Exceeded

When the rate limit is exceeded, the API returns:

- **Status Code**: `429 Too Many Requests`
- **Response Body**:
```json
{
"status": "error",
"error": "Rate limit exceeded. Too many requests."
}
```

## Client IP Detection

The rate limiter identifies clients by IP address using the following priority:

1. `X-Forwarded-For` header (for load balancers/proxies)
2. `X-Real-IP` header (for reverse proxies)
3. `RemoteAddr` from the connection (fallback)

## Implementation Details

- **Algorithm**: Sliding window rate limiter
- **Storage**: In-memory (per instance)
- **Cleanup**: Automatic cleanup of old client records every 5 minutes
- **Thread Safety**: Fully concurrent with proper mutex locking

## Best Practices for Clients

1. **Check Headers**: Always check the `X-RateLimit-*` headers to understand your current quota
2. **Handle 429 Responses**: Implement exponential backoff when receiving 429 responses
3. **Use Retry-After**: Respect the `Retry-After` header value before retrying
4. **Distribute Requests**: Avoid bursting all requests at once; distribute them evenly

## Example Usage

```bash
# Check current rate limit status
curl -I https://api.example.com/api/v1/national

# Response headers will include:
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 99

# When rate limited:
# HTTP/1.1 429 Too Many Requests
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 0
# X-RateLimit-Reset: 1672531200
# Retry-After: 60
```

## Disabling Rate Limiting

To disable rate limiting (not recommended for production):

```bash
export RATE_LIMIT_ENABLED=false
```

Or set it in your `.env` file:

```
RATE_LIMIT_ENABLED=false
```
3 changes: 2 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// @title Sulawesi Tengah COVID-19 Data API
// @version 2.1.0
// @description A comprehensive REST API for COVID-19 data in Sulawesi Tengah (Central Sulawesi), with additional national and provincial data for context. Features enhanced ODP/PDP grouping and hybrid pagination.
// @description A comprehensive REST API for COVID-19 data in Sulawesi Tengah (Central Sulawesi), with additional national and provincial data for context. Features enhanced ODP/PDP grouping, hybrid pagination, and rate limiting protection. Rate limiting: 100 requests per minute per IP address by default, with appropriate HTTP headers for client guidance.
// @termsOfService http://swagger.io/terms/
//
// @contact.name API Support
Expand Down Expand Up @@ -69,6 +69,7 @@ func main() {

router.Use(middleware.Recovery)
router.Use(middleware.Logging)
router.Use(middleware.RateLimit(cfg.RateLimit))
router.Use(middleware.CORS)

address := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
Expand Down
27 changes: 25 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (
)

type Config struct {
Database DatabaseConfig
Server ServerConfig
Database DatabaseConfig
Server ServerConfig
RateLimit RateLimitConfig
}

type DatabaseConfig struct {
Expand All @@ -31,6 +32,13 @@ type ServerConfig struct {
Host string
}

type RateLimitConfig struct {
Enabled bool
RequestsPerMinute int
BurstSize int
WindowSize time.Duration
}

func Load() *Config {
if err := godotenv.Load(); err != nil {
log.Println("No .env file found, using environment variables or defaults")
Expand All @@ -52,6 +60,12 @@ func Load() *Config {
Port: getEnvAsInt("SERVER_PORT", 8080),
Host: getEnv("SERVER_HOST", "localhost"),
},
RateLimit: RateLimitConfig{
Enabled: getEnvAsBool("RATE_LIMIT_ENABLED", true),
RequestsPerMinute: getEnvAsInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 100),
BurstSize: getEnvAsInt("RATE_LIMIT_BURST_SIZE", 20),
WindowSize: getEnvAsDuration("RATE_LIMIT_WINDOW_SIZE", 1*time.Minute),
},
}
}

Expand Down Expand Up @@ -79,3 +93,12 @@ func getEnvAsDuration(key string, defaultValue time.Duration) time.Duration {
}
return defaultValue
}

func getEnvAsBool(key string, defaultValue bool) bool {
if value := os.Getenv(key); value != "" {
if boolValue, err := strconv.ParseBool(value); err == nil {
return boolValue
}
}
return defaultValue
}
7 changes: 6 additions & 1 deletion internal/handler/covid_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@ func NewCovidHandler(covidService service.CovidService, db *database.DB) *CovidH
// @Param sort query string false "Sort by field:order (e.g., date:desc, positive:asc). Default: date:asc"
// @Success 200 {object} Response{data=[]models.NationalCaseResponse}
// @Failure 400 {object} Response
// @Failure 429 {object} Response "Rate limit exceeded"
// @Failure 500 {object} Response
// @Header 200 {string} X-RateLimit-Limit "Request limit per window"
// @Header 200 {string} X-RateLimit-Remaining "Requests remaining in current window"
// @Header 429 {string} X-RateLimit-Reset "Unix timestamp when rate limit resets"
// @Header 429 {string} Retry-After "Seconds to wait before retrying"
// @Router /national [get]
func (h *CovidHandler) GetNationalCases(w http.ResponseWriter, r *http.Request) {
startDate := r.URL.Query().Get("start_date")
endDate := r.URL.Query().Get("end_date")

// Parse sort parameters (default: date ascending)
sortParams := utils.ParseSortParam(r, "date")

Expand Down
4 changes: 2 additions & 2 deletions internal/handler/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func SetupRoutes(covidService service.CovidService, db *database.DB) *mux.Router

// Swagger documentation
router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler).Methods("GET")
// Redirect root to swagger docs for convenience

// Redirect root to swagger docs for convenience
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/swagger/index.html", http.StatusFound)
}).Methods("GET")
Expand Down
Loading
Loading