feat: add rate-limiter and test #3

Merged
salmaelsoly merged 6 commits from tests-refactoring into development 2026-03-16 11:24:18 +00:00
Member

#1

#1
feat: update dependencies and add tests for IP handling
Some checks failed
CI / test (push) Failing after 58s
cba86ae8b0
refactor: restructure app intialization and simplify test
Some checks failed
CI / test (push) Failing after 1m19s
2be9924eff
feat: add rate limiter and update app
All checks were successful
CI / test (push) Successful in 59s
02df471ca4
Owner

@salmaelsoly
rate limit should be per ip.
We could have a lot of requests that are legitimate, and then the rate limit would be some number we need to tune and adapt in function of how big our universe gets.
rate limiting to a few request per ip per 10 seconds would then be a lot more in line with ratelimiting some bad actor

@salmaelsoly rate limit should be per ip. We could have a lot of requests that are legitimate, and then the rate limit would be some number we need to tune and adapt in function of how big our universe gets. rate limiting to a few request per ip per 10 seconds would then be a lot more in line with ratelimiting some bad actor
salmaelsoly changed title from feat: add rate-limiter and test to WIP: feat: add rate-limiter and test 2026-03-12 12:16:59 +00:00
refcator: extract ip resolution helper
All checks were successful
CI / test (push) Successful in 1m3s
2f4329b930
Author
Member

@delandtj wrote in #3 (comment):

rate limit should be per ip.
We could have a lot of requests that are legitimate, and then the rate limit would be some number we need to tune and adapt in function of how big our universe gets.
rate limiting to a few request per ip per 10 seconds would then be a lot more in line with ratelimiting some bad actor

Hi!, Just wanted to point out that if you check rate_limiter.rs, you'll notice the rate limiting is indeed already per ip each client gets their own sliding window counter keyed by their ip address. So legitimate traffic from different ips won't affect each other.

@delandtj wrote in https://forge.ourworld.tf/geomind_code/geoip/pulls/3#issuecomment-11844: > rate limit should be per ip. > We could have a lot of requests that are legitimate, and then the rate limit would be some number we need to tune and adapt in function of how big our universe gets. > rate limiting to a few request per ip per 10 seconds would then be a lot more in line with ratelimiting some bad actor Hi!, Just wanted to point out that if you check rate_limiter.rs, you'll notice the rate limiting is indeed already per ip each client gets their own sliding window counter keyed by their ip address. So legitimate traffic from different ips won't affect each other.
salmaelsoly changed title from WIP: feat: add rate-limiter and test to feat: add rate-limiter and test 2026-03-12 13:05:44 +00:00
rawdaGastan requested changes 2026-03-15 11:19:52 +00:00
Dismissed
@ -0,0 +48,4 @@
let result: geoip2::City = lookup
.decode()
.map_err(|e| LookupError::Database(e.to_string()))?
.unwrap_or_default();
Member

is it okay to use default here?

is it okay to use default here?
Author
Member

it was okay since i check on the none fields of struct after that however i changed to directly return error

it was okay since i check on the none fields of struct after that however i changed to directly return error
salmaelsoly marked this conversation as resolved
@ -0,0 +81,4 @@
return ip_str.parse().map_err(|e| format!("invalid IP: {e}"));
}
if let Some(header_val) = headers.get("X-Real-IP") {
Member

X-Real-IP is just an HTTP header. Any user can set it to anything

X-Real-IP is just an HTTP header. Any user can set it to anything
Author
Member

it is used when running behind a reverse proxy, should we remove it?

it is used when running behind a reverse proxy, should we remove it?
rawdaGastan marked this conversation as resolved
@ -0,0 +38,4 @@
}
pub fn check(&self, key: &str) -> CheckResult {
let mut counters = self.counters.lock().unwrap_or_else(|e| e.into_inner());
Member

we can log warning here .unwrap_or_else(|e| { tracing::error!("rate limiter mutex poisoned, recovering"); e.into_inner() }) to not silently recover

we can log warning here `.unwrap_or_else(|e| { tracing::error!("rate limiter mutex poisoned, recovering"); e.into_inner() })` to not silently recover
salmaelsoly marked this conversation as resolved
@ -0,0 +53,4 @@
let elapsed = now.duration_since(counter.window_start);
if elapsed >= self.window_duration {
let windows_passed = elapsed.as_secs() / self.window_duration.as_secs();
Member

Integer division truncates windows_passed, some case we could be almost 2 windows and because of it the windows_passed will handle it as one window
we should edit window start to be counter.window_start += self.window_duration * windows_passed;

Integer division truncates windows_passed, some case we could be almost 2 windows and because of it the windows_passed will handle it as one window we should edit window start to be `counter.window_start += self.window_duration * windows_passed; `
salmaelsoly marked this conversation as resolved
@ -0,0 +61,4 @@
} else {
counter.previous_count = 0;
counter.current_count = 0;
counter.window_start = now;
Member

here too, this means the new window starts at an arbitrary time (whenever this request happened to arrive), not at a consistent aligned boundary.

here too, this means the new window starts at an arbitrary time (whenever this request happened to arrive), not at a consistent aligned boundary.
salmaelsoly marked this conversation as resolved
@ -0,0 +88,4 @@
}
pub fn cleanup(&self) {
let mut counters = self.counters.lock().unwrap_or_else(|e| e.into_inner());
Member

we can log warning here .unwrap_or_else(|e| { tracing::error!("rate limiter mutex poisoned, recovering"); e.into_inner() }) to not silently recover

we can log warning here `.unwrap_or_else(|e| { tracing::error!("rate limiter mutex poisoned, recovering"); e.into_inner() })` to not silently recover
salmaelsoly marked this conversation as resolved
@ -0,0 +126,4 @@
("X-RateLimit-Limit", max_requests),
("X-RateLimit-Window", window_seconds.clone()),
("X-RateLimit-Remaining", "0".to_string()),
("Retry-After", window_seconds),
Member

this makes the user always wait the same window seconds and is supposed to tell the user ( how many seconds to wait before retrying - should wait only the remaining time in window

let elapsed_in_window = now.duration_since(counter.window_start);
let retry_after = limiter.window_duration
    .saturating_sub(elapsed_in_window)
    .as_secs();
this makes the user always wait the same window seconds and is supposed to tell the user ( how many seconds to wait before retrying - should wait only the remaining time in window ``` let elapsed_in_window = now.duration_since(counter.window_start); let retry_after = limiter.window_duration .saturating_sub(elapsed_in_window) .as_secs(); ```
salmaelsoly marked this conversation as resolved
salmaelsoly merged commit 27a75e641b into development 2026-03-16 11:24:18 +00:00
salmaelsoly deleted branch tests-refactoring 2026-03-16 11:24:33 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
geomind_code/geoip!3
No description provided.