Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rate limited http transport #52

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
15 changes: 15 additions & 0 deletions _example/client/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/go-chi/httprate/_example

go 1.23.2

replace github.com/go-chi/httprate => ../../

require (
github.com/go-chi/httprate v0.0.0-00010101000000-000000000000
github.com/go-chi/transport v0.5.0
)

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
golang.org/x/time v0.9.0 // indirect
)
9 changes: 9 additions & 0 deletions _example/client/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/go-chi/transport v0.5.0 h1:xpnYcIOpBRrduJD68gX9YxkJouRGIE1y+rK5yGYnMXE=
github.com/go-chi/transport v0.5.0/go.mod h1:uoCleTaQiFtoatEiiqcXFZ5OxIp6s1DfGeVsCVbalT4=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
64 changes: 64 additions & 0 deletions _example/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"

"github.com/go-chi/httprate"
"github.com/go-chi/transport"
)

type loginPayload struct {
Username string `json:"username"`
Password string `json:"password"`
}

func main() {
ctx := context.Background()
cl := &http.Client{
Transport: transport.Chain(http.DefaultTransport, httprate.RateLimitedRequest(httprate.RPMLimit(10), 1)),
}

req := &loginPayload{
Username: "alice",
Password: "password",
}

payload, err := json.Marshal(req)
if err != nil {
log.Fatal(err)
}

for {
func() {
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()

// login accepts only 5req/mint so it gets rate limited eventually
req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost:3333/login", bytes.NewReader(payload))
if err != nil {
log.Fatal(err)
}

fmt.Println("request started", time.Now())

resp, err := cl.Do(req)
if err != nil {
log.Fatal(err)
}

defer resp.Body.Close()

if resp.StatusCode == http.StatusTooManyRequests {
fmt.Println("rate limited")
}

fmt.Println()
}()
}
}
6 changes: 5 additions & 1 deletion _example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ require (
github.com/go-chi/httprate v0.0.0-00010101000000-000000000000
)

require github.com/cespare/xxhash/v2 v2.3.0 // indirect
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-chi/transport v0.5.0 // indirect
golang.org/x/time v0.9.0 // indirect
)
5 changes: 5 additions & 0 deletions _example/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/transport v0.5.0 h1:xpnYcIOpBRrduJD68gX9YxkJouRGIE1y+rK5yGYnMXE=
github.com/go-chi/transport v0.5.0/go.mod h1:uoCleTaQiFtoatEiiqcXFZ5OxIp6s1DfGeVsCVbalT4=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ module github.com/go-chi/httprate

go 1.17

require github.com/cespare/xxhash/v2 v2.3.0

require golang.org/x/sync v0.7.0 // indirect
require (
github.com/cespare/xxhash/v2 v2.3.0
github.com/go-chi/transport v0.5.0
golang.org/x/sync v0.7.0
golang.org/x/time v0.9.0
)
7 changes: 5 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/go-chi/transport v0.5.0 h1:xpnYcIOpBRrduJD68gX9YxkJouRGIE1y+rK5yGYnMXE=
github.com/go-chi/transport v0.5.0/go.mod h1:uoCleTaQiFtoatEiiqcXFZ5OxIp6s1DfGeVsCVbalT4=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
28 changes: 28 additions & 0 deletions transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package httprate

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

"github.com/go-chi/transport"
"golang.org/x/time/rate"
)

func RateLimitedRequest(limit rate.Limit, burst int) func(http.RoundTripper) http.RoundTripper {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP rate-limit outgoing requests. Nice 👍

This is a good start. Few notes:

  • Could we reuse the existing LimitCounter implementations (same as for incoming requests), if that's possible? Meaning we would reuse the existing in-memory counter and redis counter.
  • Let's hide "golang.org/x/time/rate" from the pkg API (FYI, this package doesn't use limit/burst - it uses sliding window counter instead)
  • Can you think of the package API a bit?
    • Do we need to rate-limit 1) a specific domain (e.g. key: httprate:example.com); 2) or all outgoing requests made through the *http.Client? -- is (2) enough?
    •   package httprate
        
        func Client(c *http.Client, requestLimit int, windowLength time.Duration, key string, ...opts) *httprate.LimitClient 
        
        func Transport(rt http.RoundTripper, requestLimit int, windowLength time.Duration, key string, ...opts) http.RoundTripper

limiter := rate.NewLimiter(limit, burst)

return func(next http.RoundTripper) http.RoundTripper {
return transport.RoundTripFunc(func(req *http.Request) (resp *http.Response, err error) {
if err := limiter.Wait(req.Context()); err != nil {
return nil, fmt.Errorf("rate limiter wait: %w", err)
}

return next.RoundTrip(req)
})
}
}

func RPMLimit(rpm uint) rate.Limit {
return rate.Every(time.Minute / time.Duration(rpm))
}
Loading