Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c8a566c
initial implementation for distributed and local storage rate limiters
Umang01-hash Sep 10, 2025
2048c54
fix linters and other errors in the initial implementation
Umang01-hash Sep 10, 2025
1d28995
fix bugs in the implementation after testing
Umang01-hash Sep 11, 2025
4e6ecd7
add documentation
Umang01-hash Sep 11, 2025
a651ea7
add test for local rate limiter implementation
Umang01-hash Sep 16, 2025
2b78265
Merge remote-tracking branch 'origin' into en/rate_limiter
Umang01-hash Sep 16, 2025
e826318
fix test
Umang01-hash Sep 16, 2025
51616b5
fix rate limiter concurrency test
Umang01-hash Sep 16, 2025
71d4f72
fix linters
Umang01-hash Sep 17, 2025
aae16f3
Merge remote-tracking branch 'origin' into en/rate_limiter
Umang01-hash Sep 25, 2025
a104474
make time window generic
Umang01-hash Sep 25, 2025
8c8b29b
update documentation
Umang01-hash Sep 25, 2025
1217084
Merge remote-tracking branch 'origin' into en/rate_limiter
Umang01-hash Sep 29, 2025
12bb728
resolve review comments
Umang01-hash Sep 29, 2025
51d3388
replace concrete rate limiter stores with interface
Umang01-hash Sep 29, 2025
29b15e2
Merge remote-tracking branch 'origin' into en/rate_limiter
Umang01-hash Sep 29, 2025
a90d0e2
add more tests
Umang01-hash Sep 30, 2025
fd7fe70
refactor implementation to unify the structs and remove duplicate codes
Umang01-hash Sep 30, 2025
815dec7
re-write tests
Umang01-hash Oct 3, 2025
82623af
Merge remote-tracking branch 'origin' into en/rate_limiter
Umang01-hash Oct 3, 2025
c54c78e
revert unwanted changes
Umang01-hash Oct 3, 2025
8e6f6ea
remove changes in interface of logger and metrics
Umang01-hash Oct 3, 2025
8568065
fix linters
Umang01-hash Oct 3, 2025
bb387ea
refactoring implementation
Umang01-hash Oct 3, 2025
f9f7b04
build(deps): update github.com/grpc-ecosystem/go-grpc-middleware to v2
Juneezee Oct 3, 2025
8bfd3fb
refactor(docs): replace {serviceName} placeholders with <SERVICE_NAME…
NishantRajZop Sep 27, 2025
8665967
Merge remote-tracking branch 'origin' into en/rate_limiter
Umang01-hash Oct 6, 2025
2d2faa3
Merge remote-tracking branch 'origin' into en/rate_limiter
Umang01-hash Nov 19, 2025
09385b5
update from development
Umang01-hash Nov 19, 2025
05f3fe8
resolve linters and fix tests
Umang01-hash Nov 19, 2025
f39e8f2
fix linter error in test
Umang01-hash Nov 19, 2025
d9c8d75
add rate limiter in http service example
Umang01-hash Nov 21, 2025
def83ad
Merge branch 'development' into en/rate_limiter
Umang01-hash Nov 21, 2025
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
30 changes: 29 additions & 1 deletion docs/advanced-guide/http-communication/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,26 @@ GoFr provides its user with additional configurational options while registering
- **DefaultHeaders** - This option allows user to set some default headers that will be propagated to the downstream HTTP Service every time it is being called.
- **HealthConfig** - This option allows user to add the `HealthEndpoint` along with `Timeout` to enable and perform the timely health checks for downstream HTTP Service.
- **RetryConfig** - This option allows user to add the maximum number of retry count if before returning error if any downstream HTTP Service fails.
- **RateLimiterConfig** - This option allows user to configure rate limiting for downstream service calls using token bucket algorithm. It controls the request rate to prevent overwhelming dependent services and supports both in-memory and Redis-based implementations.

**Rate Limiter Store: Customization**
GoFr allows you to use a custom rate limiter store by implementing the RateLimiterStore interface.This enables integration with any backend (e.g., Redis, database, or custom logic)

**Interface:**

```go
type RateLimiterStore interface {
Allow(ctx context.Context, key string, config RateLimiterConfig) (allowed bool, retryAfter time.Duration, err error)
StartCleanup(ctx context.Context)
StopCleanup()
}
```

#### Usage:

```go
rc := redis.NewClient(a.Config, a.Logger(), a.Metrics())

a.AddHTTPService("cat-facts", "https://catfact.ninja",
service.NewAPIKeyConfig("some-random-key"),
service.NewBasicAuthConfig("username", "password"),
Expand All @@ -119,5 +135,17 @@ a.AddHTTPService("cat-facts", "https://catfact.ninja",
&service.RetryConfig{
MaxRetries: 5
},

&service.RateLimiterConfig{
Requests: 5,
Window: time.Minute,
Burst: 10,
Store: service.NewRedisRateLimiterStore(rc)}, // Skip this field to use in-memory store
},
)
```
```

**Best Practices:**
- For distributed systems: It is strongly recommended to use Redis-based store (`NewRedisRateLimiterStore`) to ensure consistent rate limiting across multiple instances of your application.
- For single-instance applications: The default in-memory store (`NewLocalRateLimiterStore`) is sufficient and provides better performance.
- Rate configuration: Set Burst higher than Requests to allow short traffic bursts while maintaining average rate limits.
5 changes: 5 additions & 0 deletions examples/using-http-service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ func main() {
Threshold: 4,
Interval: 1 * time.Second,
},
&service.RateLimiterConfig{
Requests: 10,
Window: time.Second,
Burst: 15,
},
&service.HealthConfig{
HealthEndpoint: "breeds",
},
Expand Down
3 changes: 2 additions & 1 deletion pkg/gofr/service/mock_metrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/gofr/service/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func validateTokenURL(tokenURL string) error {
return AuthErr{nil, "invalid host pattern, contains `..`"}
case strings.HasSuffix(u.Host, "."):
return AuthErr{nil, "invalid host pattern, ends with `.`"}
case u.Scheme != "http" && u.Scheme != "https":
case u.Scheme != methodHTTP && u.Scheme != methodHTTPS:
return AuthErr{nil, "invalid scheme, allowed http and https only"}
default:
return nil
Expand Down
218 changes: 218 additions & 0 deletions pkg/gofr/service/rate_limiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package service

import (
"context"
"net/http"
"strings"
"time"
)

const (
defaultRequestsPerMinute = 60
defaultBurstCapacity = 10
defaultWindow = time.Minute
)

// rateLimiter provides unified rate limiting for HTTP clients.
type rateLimiter struct {
config RateLimiterConfig
store RateLimiterStore
HTTP // Embedded HTTP service
}

// NewRateLimiter creates a new unified rate limiter.
func NewRateLimiter(config RateLimiterConfig, h HTTP) HTTP {
rl := &rateLimiter{
config: config,
store: config.Store,
HTTP: h,
}

// Start cleanup routine
ctx := context.Background()
rl.store.StartCleanup(ctx)

return rl
}

// AddOption allows RateLimiterConfig to be used as a service.Options.
func (config *RateLimiterConfig) AddOption(h HTTP) HTTP {
// Validate always succeeds - it auto-corrects invalid values
_ = config.Validate()

// Assume cfg is already validated via constructor
if config.Store == nil {
config.Store = NewLocalRateLimiterStore()
}

return NewRateLimiter(*config, h)
}

// buildFullURL constructs an absolute URL by combining the base service URL with the given path.
func (rl *rateLimiter) buildFullURL(path string) string {
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
return path
}

// Get base URL from embedded HTTP service
httpSvcImpl, ok := rl.HTTP.(*httpService)
if !ok {
return path
}

base := strings.TrimRight(httpSvcImpl.url, "/")
if base == "" {
return path
}

// Ensure path starts with /
if !strings.HasPrefix(path, "/") {
path = "/" + path
}

return base + path
}

// checkRateLimit performs rate limit check using the configured store.
func (rl *rateLimiter) checkRateLimit(req *http.Request) error {
serviceKey := rl.config.KeyFunc(req)

allowed, retryAfter, err := rl.store.Allow(req.Context(), serviceKey, rl.config)
if err != nil {
return nil // Fail open
}

if !allowed {
return &RateLimitError{ServiceKey: serviceKey, RetryAfter: retryAfter}
}

return nil
}

// Get performs rate-limited HTTP GET request.
func (rl *rateLimiter) Get(ctx context.Context, path string, queryParams map[string]any) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.Get(ctx, path, queryParams)
}

// GetWithHeaders performs rate-limited HTTP GET request with custom headers.
func (rl *rateLimiter) GetWithHeaders(ctx context.Context, path string, queryParams map[string]any,
headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.GetWithHeaders(ctx, path, queryParams, headers)
}

// Post performs rate-limited HTTP POST request.
func (rl *rateLimiter) Post(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.Post(ctx, path, queryParams, body)
}

// PostWithHeaders performs rate-limited HTTP POST request with custom headers.
func (rl *rateLimiter) PostWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers)
}

// Put performs rate-limited HTTP PUT request.
func (rl *rateLimiter) Put(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPut, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.Put(ctx, path, queryParams, body)
}

// PutWithHeaders performs rate-limited HTTP PUT request with custom headers.
func (rl *rateLimiter) PutWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPut, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers)
}

// Patch performs rate-limited HTTP PATCH request.
func (rl *rateLimiter) Patch(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPatch, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.Patch(ctx, path, queryParams, body)
}

// PatchWithHeaders performs rate-limited HTTP PATCH request with custom headers.
func (rl *rateLimiter) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPatch, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers)
}

// Delete performs rate-limited HTTP DELETE request.
func (rl *rateLimiter) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodDelete, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.Delete(ctx, path, body)
}

// DeleteWithHeaders performs rate-limited HTTP DELETE request with custom headers.
func (rl *rateLimiter) DeleteWithHeaders(ctx context.Context, path string, body []byte,
headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodDelete, fullURL, http.NoBody)

if err := rl.checkRateLimit(req); err != nil {
return nil, err
}

return rl.HTTP.DeleteWithHeaders(ctx, path, body, headers)
}
Loading
Loading