Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 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
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
9 changes: 9 additions & 0 deletions docs/advanced-guide/http-communication/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,13 @@ 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.

#### Usage:

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

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

&service.RateLimiterConfig{
Rate: 5,
Burst: 10,
RedisClient: rc, // if RedisClient is nil, in-memory rate limiter will be used
},
)
```
3 changes: 3 additions & 0 deletions pkg/gofr/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ func (c *Container) registerFrameworkMetrics() {
httpBuckets := []float64{.001, .003, .005, .01, .02, .03, .05, .1, .2, .3, .5, .75, 1, 2, 3, 5, 10, 30}
c.Metrics().NewHistogram("app_http_response", "Response time of HTTP requests in seconds.", httpBuckets...)
c.Metrics().NewHistogram("app_http_service_response", "Response time of HTTP service requests in seconds.", httpBuckets...)
c.Metrics().NewCounter("app_rate_limiter_requests_total", "Total rate limiter requests")
c.Metrics().NewCounter("app_rate_limiter_denied_total", "Total denied requests")
c.Metrics().NewGauge("app_rate_limiter_tokens_available", "Current tokens available")
}

{ // Redis metrics
Expand Down
8 changes: 8 additions & 0 deletions pkg/gofr/service/circuit_breaker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,14 @@ func (m *mockMetrics) RecordHistogram(ctx context.Context, name string, value fl
m.Called(ctx, name, value, labels)
}

func (m *mockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.Called(ctx, name, labels)
}

func (m *mockMetrics) SetGauge(name string, value float64, labels ...string) {
m.Called(name, value, labels)
}

type customTransport struct {
}

Expand Down
1 change: 1 addition & 0 deletions pkg/gofr/service/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

type Logger interface {
Log(args ...any)
Debug(args ...any)
}

type Log struct {
Expand Down
2 changes: 2 additions & 0 deletions pkg/gofr/service/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ package service
import "context"

type Metrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
SetGauge(name string, value float64, labels ...string)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
35 changes: 35 additions & 0 deletions 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
108 changes: 108 additions & 0 deletions pkg/gofr/service/rate_limiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package service

import (
"errors"
"fmt"
"net/http"
"time"

gofrRedis "gofr.dev/pkg/gofr/datasource/redis"
)

var (
errInvalidRequestRate = errors.New("requestsPerSecond must be greater than 0")
errInvalidBurstSize = errors.New("burst must be greater than 0")
errInvalidRedisResultType = errors.New("unexpected Redis result type")
)

// RateLimiterConfig with custom keying support.
type RateLimiterConfig struct {
RequestsPerSecond float64 // Token refill rate (must be > 0)
Burst int // Maximum burst capacity (must be > 0)
KeyFunc func(*http.Request) string // Optional custom key extraction
RedisClient *gofrRedis.Redis `json:"-"` // Optional Redis for distributed limiting
}

// defaultKeyFunc extracts a normalized service key from an HTTP request.
func defaultKeyFunc(req *http.Request) string {
if req == nil || req.URL == nil {
return "unknown"
}

scheme := req.URL.Scheme
host := req.URL.Host

if scheme == "" {
if req.TLS != nil {
scheme = methodHTTPS
} else {
scheme = methodHTTP
}
}

if host == "" {
host = req.Host
}

if host == "" {
host = unknownServiceKey
}

return scheme + "://" + host
}

// Validate checks if the configuration is valid.
func (config *RateLimiterConfig) Validate() error {
if config.RequestsPerSecond <= 0 {
return fmt.Errorf("%w: %f", errInvalidRequestRate, config.RequestsPerSecond)
}

if config.Burst <= 0 {
return fmt.Errorf("%w: %d", errInvalidBurstSize, config.Burst)
}

// Set default key function if not provided.
if config.KeyFunc == nil {
config.KeyFunc = defaultKeyFunc
}

return nil
}

// AddOption implements the Options interface.
func (config *RateLimiterConfig) AddOption(h HTTP) HTTP {
if err := config.Validate(); err != nil {
if httpSvc, ok := h.(*httpService); ok {
httpSvc.Logger.Log("Invalid rate limiter config, disabling rate limiting", "error", err)
}

return h
}

// Choose implementation based on Redis client availability.
if config.RedisClient != nil {
return NewDistributedRateLimiter(*config, h)
}

// Log warning for local rate limiting.
if httpSvc, ok := h.(*httpService); ok {
httpSvc.Logger.Log("Using local rate limiting - not suitable for multi-instance deployments")
}

return NewLocalRateLimiter(*config, h)
}

// RateLimitError represents a rate limiting error.
type RateLimitError struct {
ServiceKey string
RetryAfter time.Duration
}

func (e *RateLimitError) Error() string {
return fmt.Sprintf("rate limit exceeded for service: %s, retry after: %v", e.ServiceKey, e.RetryAfter)
}

// StatusCode Implement StatusCodeResponder so Responder picks correct HTTP code.
func (*RateLimitError) StatusCode() int {
return http.StatusTooManyRequests // 429
}
Loading
Loading