Skip to main content

max / makenotwork

Add per-IP rate limiting to search endpoint (burst 5, 1/sec) Full-text + trigram similarity queries are expensive. Separate governor from write routes to allow tighter limits on search. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-26 20:20 UTC
Commit: bbfc2eb242e0c0691d6a72fffcb14e51387ee803
Parent: 321c73e
1 file changed, +21 insertions, -1 deletion
@@ -37,6 +37,10 @@ use crate::AppState;
37 37 const WRITE_RATE_LIMIT_MS: u64 = 500;
38 38 const WRITE_RATE_LIMIT_BURST: u32 = 10;
39 39
40 + /// Search endpoint: burst 5, then 1/sec — full-text + trigram queries are expensive.
41 + const SEARCH_RATE_LIMIT_MS: u64 = 1000;
42 + const SEARCH_RATE_LIMIT_BURST: u32 = 5;
43 +
40 44 /// Build the forum route tree.
41 45 pub fn forum_routes(state: AppState) -> Router {
42 46 let write_rate_limit = std::sync::Arc::new(
@@ -85,6 +89,22 @@ pub fn forum_routes(state: AppState) -> Router {
85 89 config: write_rate_limit,
86 90 });
87 91
92 + // Search — rate limited per IP (expensive full-text queries)
93 + let search_rate_limit = std::sync::Arc::new(
94 + GovernorConfigBuilder::default()
95 + .key_extractor(SmartIpKeyExtractor)
96 + .per_millisecond(SEARCH_RATE_LIMIT_MS)
97 + .burst_size(SEARCH_RATE_LIMIT_BURST)
98 + .finish()
99 + .expect("search rate limiter config"),
100 + );
101 +
102 + let search_routes = Router::new()
103 + .route("/search", get(search::search_handler))
104 + .route_layer(GovernorLayer {
105 + config: search_rate_limit,
106 + });
107 +
88 108 // GET routes + auth + health — no rate limiting
89 109 let read_routes = Router::new()
90 110 .route("/", get(forum::forum_directory))
@@ -101,7 +121,6 @@ pub fn forum_routes(state: AppState) -> Router {
101 121 .route("/p/{slug}/{category}/{thread_id}/edit", get(forum::edit_thread_form))
102 122 .route("/tracked", get(tracking::tracked_threads_page))
103 123 .route("/about/tracking", get(tracking::tracking_info_page))
104 - .route("/search", get(search::search_handler))
105 124 .route("/_admin", get(admin::admin_dashboard))
106 125 .route("/auth/login", get(auth::login))
107 126 .route("/auth/callback", get(auth::callback))
@@ -111,6 +130,7 @@ pub fn forum_routes(state: AppState) -> Router {
111 130 .route("/api/health", get(health));
112 131
113 132 read_routes
133 + .merge(search_routes)
114 134 .merge(write_routes)
115 135 .fallback(not_found_handler)
116 136 .with_state(state)