mirror of
https://github.com/penpot/penpot.git
synced 2026-09-26 05:46:13 +00:00
* 🐛 Restore rate-limit headers and add Retry-After The account-lockout change replaced the header-forwarding 429 handler with a body-only one, so existing RPC rate-limit responses lost their x-rate-limit-remaining and x-rate-limit-reset headers. Account lockout never sent Retry-After either. Make handle-error :rate-limit preserve ::http/headers and add a retry-after header when the exception carries a non-nil :ttl in seconds, keeping the current JSON body. Add focused tests for both the lockout and the RPC limiter paths. Document activation, defaults, password/LDAP scope, Redis fail-open behavior, and the lockout risk, and record the final HTTP contract in the backend auth memory. Refs #11397 AI-assisted-by: deepseek-v4.1-flash * 🐛 Add Retry-After to RPC 429 and expose headers in CORS Address review follow-ups on the account-lockout 429 contract: - The RPC limiter now sets retry-after in its 429 headers (seconds until the longest rejecting limit resets), so it matches the account-lockout response and the HTTP standard. - CORS exposes retry-after, x-rate-limit-remaining, and x-rate-limit-reset so browser clients can read them. - Use backticks for Retry-After and account-locked in the docs for consistency with nearby sections. Refs #11397 AI-assisted-by: deepseek-v4.1-flash
67 lines
2.6 KiB
Clojure
67 lines
2.6 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
|
|
|
(ns backend-tests.rpc-rlimit-test
|
|
(:require
|
|
[app.common.time :as ct]
|
|
[app.loggers.database]
|
|
[app.loggers.mattermost]
|
|
[app.redis :as rds]
|
|
[app.rpc.rlimit :as rlimit]
|
|
[clojure.test :as t]))
|
|
|
|
(t/deftest bucket-reset-supports-fractional-milliseconds
|
|
(let [now (ct/inst 0)
|
|
limit {::rlimit/name :test
|
|
::rlimit/strategy :bucket
|
|
::rlimit/key "test"
|
|
::rlimit/method "main.test"
|
|
::rlimit/capacity 5
|
|
::rlimit/rate 3
|
|
::rlimit/interval (ct/duration 1000)
|
|
::rlimit/params [1 3 5]
|
|
::rlimit/opts "5/3/1s"}]
|
|
(with-redefs [rds/eval (fn [_ _] [true 4])]
|
|
(let [result (rlimit/process-limit nil "profile" now limit)]
|
|
(t/is (= (ct/inst 334)
|
|
(:app.rpc.rlimit.result/reset result)))))))
|
|
|
|
(defn- test-limit
|
|
[]
|
|
{::rlimit/name :test
|
|
::rlimit/strategy :bucket
|
|
::rlimit/key "test"
|
|
::rlimit/method "main.test"
|
|
::rlimit/capacity 5
|
|
::rlimit/rate 3
|
|
::rlimit/interval (ct/duration 1000)
|
|
::rlimit/params [1 3 5]
|
|
::rlimit/opts "5/3/1s"})
|
|
|
|
(t/deftest rejected-rate-limit-emits-retry-after-header
|
|
;; When a limit rejects the request the 429 headers must carry a
|
|
;; Retry-After with the seconds until the client can retry.
|
|
(with-redefs [rds/eval (fn [_ _] [false 2])
|
|
app.loggers.mattermost/emit (fn [_ _] nil)
|
|
app.loggers.database/emit (fn [_ _] nil)]
|
|
(let [result (#'rlimit/process-limits {::rds/conn nil} "profile" [(test-limit)] (ct/inst 0))
|
|
headers (::rlimit/headers result)]
|
|
(t/is (false? (::rlimit/allowed result)))
|
|
(t/is (= "1" (get headers "retry-after")))
|
|
(t/is (some? (get headers "x-rate-limit-remaining")))
|
|
(t/is (some? (get headers "x-rate-limit-reset"))))))
|
|
|
|
(t/deftest allowed-rate-limit-omits-retry-after-header
|
|
;; Retry-After is only meaningful on a rejected 429; allowed responses
|
|
;; must not carry it.
|
|
(with-redefs [rds/eval (fn [_ _] [true 4])
|
|
app.loggers.mattermost/emit (fn [_ _] nil)
|
|
app.loggers.database/emit (fn [_ _] nil)]
|
|
(let [result (#'rlimit/process-limits {::rds/conn nil} "profile" [(test-limit)] (ct/inst 0))
|
|
headers (::rlimit/headers result)]
|
|
(t/is (true? (::rlimit/allowed result)))
|
|
(t/is (nil? (get headers "retry-after"))))))
|