Skip to content
89 linesCodeBlameRaw
1//! Rate limits: Workers Rate Limiting bindings, asked once per request with
2//! a key (a client's address, a repository's id, a hash of a token).
3//!
4//! Every binding, its namespace id and its limit is listed in `RATE_LIMITS`
5//! (packages/contracts/src/rate-limits.ts), which apps/web's tests check
6//! each wrangler.jsonc against; docs/RATE-LIMITS.md says why each is what
7//! it is.
8//!
9//! A limit fails open: a binding that is not there (self-hosted) or that
10//! fails lets the request through. A limit guards against floods; it is
11//! never a reason for g1t to stop answering.
12
13use worker::Env;
14
15/// Every limit's window, in seconds, and how long a client past one is
16/// told to wait (`Retry-After`). Workers Rate Limiting counts over 10 or 60.
17pub const PERIOD_SECONDS: u32 = 60;
18
19/// What asking a limit said. Only `Limited` refuses the request.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Verdict {
22 Allowed,
23 Limited,
24 /// No binding, or it failed: let through.
25 Unavailable,
26}
27
28impl Verdict {
29 pub fn limited(self) -> bool {
30 self == Verdict::Limited
31 }
32}
33
34/// The verdict from what the binding answered: `None` when there is no
35/// binding, `Some(Err)` when asking it failed, else whether it let the
36/// request through.
37pub fn verdict<E>(answered: Option<std::result::Result<bool, E>>) -> Verdict {
38 match answered {
39 Some(Ok(true)) => Verdict::Allowed,
40 Some(Ok(false)) => Verdict::Limited,
41 Some(Err(_)) | None => Verdict::Unavailable,
42 }
43}
44
45/// Counts one request against the binding named `binding` under `key`.
46/// Never fails; a failure to ask is logged and lets the request through.
47pub async fn check(env: &Env, binding: &str, key: String) -> Verdict {
48 let Ok(limiter) = env.rate_limiter(binding) else {
49 return Verdict::Unavailable;
50 };
51 let answered = limiter.limit(key).await.map(|outcome| outcome.success);
52 if let Err(problem) = &answered {
53 worker::console_error!("rate limit {binding} could not be asked: {problem}");
54 }
55 verdict(Some(answered))
56}
57
58/// A client's address for a key: `ip:` and `CF-Connecting-IP`, or
59/// `ip:unknown` when there is none (local development).
60pub fn address_key(address: Option<&str>) -> String {
61 format!("ip:{}", address.map(str::trim).filter(|a| !a.is_empty()).unwrap_or("unknown"))
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn only_a_limit_the_binding_says_is_reached_refuses() {
70 assert_eq!(verdict::<()>(Some(Ok(true))), Verdict::Allowed);
71 assert_eq!(verdict::<()>(Some(Ok(false))), Verdict::Limited);
72 assert!(verdict::<()>(Some(Ok(false))).limited());
73 }
74
75 #[test]
76 fn no_binding_or_a_failing_one_lets_requests_through() {
77 assert_eq!(verdict::<()>(None), Verdict::Unavailable);
78 assert_eq!(verdict(Some(Err("binding threw"))), Verdict::Unavailable);
79 assert!(!verdict::<()>(None).limited());
80 assert!(!verdict(Some(Err("binding threw"))).limited());
81 }
82
83 #[test]
84 fn addresses_are_keyed_as_given_or_unknown() {
85 assert_eq!(address_key(Some(" 203.0.113.9 ")), "ip:203.0.113.9");
86 assert_eq!(address_key(Some("")), "ip:unknown");
87 assert_eq!(address_key(None), "ip:unknown");
88 }
89}