valpackett / ring-ratelimit

Rate limiting middleware for Clojure Ring | now on https://codeberg.org/valpackett/ring-ratelimit
https://codeberg.org/valpackett/ring-ratelimit
79 stars 7 forks source link

`wrap-ratelimit` always overwrites X-RateLimit-Limit/X-RateLimit-Remaining headers #6

Open galatyn opened 5 years ago

galatyn commented 5 years ago

If there are more than one limiter for same request, then headers X-RateLimit-Limit/X-RateLimit-Remaining can be incorrect. Example:

  1. wrap-ratelimit creates header with Limit=100, Remainig=99 and calls next handler.
  2. Next wrap-ratelimit in the chain creates header Limit=100, Remainig=0 (call is blocked).
  3. wrap-ratelimit from step 1. overwrites X-RateLimit-Limit/X-RateLimit-Remaining. Expected result: X-RateLimit-Limit=100 / X-RateLimit-Remaining=0 Actual result: X-RateLimit-Limit=100 / X-RateLimit-Remaining=99 Possible solution is to return min values:
(defn wrap-ratelimit
  ;; Modified version of `ring.middleware.ratelimit/wrap-ratelimit`.
  ([handler] (wrap-ratelimit handler {}))
  ([handler config]
   (let [config* (merge (ratelimit/default-config) config)
         limits (:limits config*)
         backend (:backend config*)
         err-handler (:err-handler config*)]
     (fn [req]
       (if (available? backend)
         (do
           (when (not= (get-hour backend) (current-hour))
             (reset-limits! backend (current-hour)))
           (if-let [limiter (first (filter #((:filter %) req) limits))]
             (let [limit (:limit limiter)
                   thekey (str (:key-prefix limiter) ((:getter limiter) req))
                   current (get-limit backend limit thekey)
                   remaining (- limit current)
                   is-over (< remaining 0)
                   h (if is-over err-handler handler)
                   rsp (h req)
                   parse-pos-or-zero (fn [str default]
                                       (if (and str (re-matches #"\d+" str))
                                         (Integer/parseInt str)
                                         default))
                   h-limit (-> (parse-pos-or-zero (get-in rsp [:headers "X-RateLimit-Limit"]) limit)
                               (min limit))
                   h-remaining (-> (parse-pos-or-zero (get-in rsp [:headers "X-RateLimit-Remaining"]) remaining)
                                   (min remaining))
                   rl-headers {"X-RateLimit-Limit" (str h-limit)
                               "X-RateLimit-Remaining" (str (max 0 h-remaining))}]
               (assoc rsp :headers (merge (:headers rsp) rl-headers)))
             (handler req)))
         (handler req))))))
valpackett commented 5 years ago

Hi, this package is not really maintained (I haven't used Clojure in years). If you submit a fully ready pull request, I'll merge, but I'm not going to spend time on testing and stuff. :)