ulule / limiter

Dead simple rate limit middleware for Go.
MIT License
2.08k stars 150 forks source link

http.Client rate limited http.Transport #14

Open dougnukem opened 8 years ago

dougnukem commented 8 years ago

Overview

It'd be nice if this library supported a way for http.Client outgoing requests to utilizing limiter.Limit to rate limit based on configured API rate limits useful for things like:

I think you'd want to be able to configure a limit.Limiter per URL request path e.g.:

Similar to how server-side HTTP handlers are setup:

mux.HandleFunc("/1.1/statuses/user_timeline.json", func(r *http.Request) error {
    c, err := userTimelineLimiter.Get("user_timeline.json")
    if err != nil {
      return err
    }

    if c.Reached {
      // client policy:
      // 1. wait and retry
      // 2. error rate limit
      // 3. simulate http response error with rate limit
      err := handleRatelimitReached(r)
    }
})

Maybe even use http.NewServeMux to client-side proxy rate limit responses?

There's some example of it here:


// RateLimitedTransport is used to conform to rate limits that are
// communicated through "X-RateLimit-" headers, like GitHub's API. It
// implements http.RoundTripper and can be used for configuring a http.Client.
type RateLimitedTransport struct {
    Base http.RoundTripper
}

func (t *RateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    res, err := t.base().RoundTrip(req)
    if err != nil {
        return res, err
    }

    // Fetch headers
    remStr := res.Header.Get("X-RateLimit-Remaining")
    if remStr == "" {
        return res, err
    }
    resetStr := res.Header.Get("X-RateLimit-Reset")
    if resetStr == "" {
        return res, err
    }

    rem, err := strconv.Atoi(remStr)
    if err != nil {
        return res, err
    }
    epoch, err := strconv.ParseInt(resetStr, 10, 64)
    if err != nil {
        return res, err
    }
    reset := time.Unix(epoch, 0)

    // Determine sleep time
    untilReset := reset.Sub(time.Now())
    delay := time.Duration(float64(untilReset) / (float64(rem) + 1))
    time.Sleep(delay)

    return res, err
}
dougnukem commented 8 years ago

Looking at httpcontrol

Seems like there's a good example of intercepting an HTTP request and redirecting the response or also using the real response (e.g. if Rate limit is exceeded).

// A cache enabled RoundTrip.
func (t *Transport) RoundTrip(req *http.Request) (res *http.Response, err error) {
    key := t.Config.Key(req)
    var entry cacheEntry

    // from cache
    if key != "" {
        raw, err := t.ByteCache.Get(key)
        if err != nil {
            return nil, err
        }

        if raw != nil {
            if err = json.Unmarshal(raw, &entry); err != nil {
                return nil, err
            }

            // setup fake http.Response
            res = entry.Response
            res.Body = ioutil.NopCloser(bytes.NewReader(entry.Body))
            res.Request = req
            return res, nil
        }
    }

    // real request
    res, err = t.Transport.RoundTrip(req)
    if err != nil {
        return nil, err
    }

    // no caching required
    if key == "" {
        return res, nil
    }

    // fully buffer response for caching purposes
    body, err := ioutil.ReadAll(res.Body)
    res.Body.Close()
    if err != nil {
        return nil, err
    }

    // remove properties we want to skip in serialization
    res.Body = nil
    res.Request = nil

    // serialize the cache entry
    entry.Response = res
    entry.Body = body
    raw, err := json.Marshal(&entry)
    if err != nil {
        return nil, err
    }

    // put back non serialized properties
    res.Body = ioutil.NopCloser(bytes.NewReader(body))
    res.Request = req

    // determine timeout & put it in cache
    timeout := t.Config.MaxAge(res)
    if timeout != 0 {
        if err = t.ByteCache.Store(key, raw, timeout); err != nil {
            return nil, err
        }
    }

    // reset body in case the config.Timeout logic consumed it
    res.Body = ioutil.NopCloser(bytes.NewReader(body))
    return res, nil
}