tenseleyflow/shithub / 334b0c7

Browse files

search: wire ratelimit.Limiter into /search Mount (SR2 H4 wiring)

Threads a separate ratelimit.New(pool) into buildSearchHandlers so
SR2 H4's middleware actually fires on the live route. Shares the
DB-backed counter table with auth's RateLimiter; segregated by
Policy.Scope='search'.

Tests covering H4 use a constructed limiter directly (no DB needed
beyond a stub pool) — see search.go's documented Mount path.

Together with the previous commit (H5 tab-count cache), /search
goes from worst-case 6 FTS queries per request unrate-limited to
1 query per request on cache hit + 60/min/viewer hard ceiling.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
334b0c741555f0fc5e2917db6a75aa88147057af
Parents
fe289bb
Tree
25f0286

2 changed files

StatusFile+-
M internal/web/search_wiring.go 8 1
M internal/web/server.go 6 1
internal/web/search_wiring.gomodified
@@ -8,22 +8,29 @@ import (
88
 
99
 	"github.com/jackc/pgx/v5/pgxpool"
1010
 
11
+	"github.com/tenseleyFlow/shithub/internal/ratelimit"
1112
 	searchhandlers "github.com/tenseleyFlow/shithub/internal/web/handlers/search"
1213
 	"github.com/tenseleyFlow/shithub/internal/web/render"
1314
 )
1415
 
1516
 // buildSearchHandlers wires the S28 search handler set. Owns its own
1617
 // renderer (same pattern as the other handler builders).
18
+//
19
+// Limiter is the per-request rate-limit entrypoint applied to /search
20
+// (audit 2026-05-10 H4). Pass nil from tests when you don't care
21
+// about throttling; production wiring constructs one shared limiter
22
+// per shithubd-web process and threads it everywhere.
1723
 func buildSearchHandlers(
1824
 	pool *pgxpool.Pool,
1925
 	tmplFS fs.FS,
2026
 	logger *slog.Logger,
27
+	limiter *ratelimit.Limiter,
2128
 ) (*searchhandlers.Handlers, error) {
2229
 	rr, err := render.New(tmplFS, render.Options{Octicons: render.BuiltinOcticons()})
2330
 	if err != nil {
2431
 		return nil, err
2532
 	}
2633
 	return searchhandlers.New(searchhandlers.Deps{
27
-		Logger: logger, Render: rr, Pool: pool,
34
+		Logger: logger, Render: rr, Pool: pool, Limiter: limiter,
2835
 	})
2936
 }
internal/web/server.gomodified
@@ -30,6 +30,7 @@ import (
3030
 	infralog "github.com/tenseleyFlow/shithub/internal/infra/log"
3131
 	"github.com/tenseleyFlow/shithub/internal/infra/metrics"
3232
 	"github.com/tenseleyFlow/shithub/internal/infra/tracing"
33
+	"github.com/tenseleyFlow/shithub/internal/ratelimit"
3334
 	"github.com/tenseleyFlow/shithub/internal/version"
3435
 	"github.com/tenseleyFlow/shithub/internal/web/handlers"
3536
 	"github.com/tenseleyFlow/shithub/internal/web/middleware"
@@ -221,7 +222,11 @@ func Run(ctx context.Context, opts Options) error {
221222
 		deps.RepoSocialMounter = repoH.MountSocial
222223
 		deps.RepoForkMounter = repoH.MountFork
223224
 
224
-		searchH, err := buildSearchHandlers(pool, deps.TemplatesFS, logger)
225
+		// Search gets its own Limiter wired around /search +
226
+		// /search/quick (audit 2026-05-10 H4). Independent instance
227
+		// from auth's RateLimiter; both share DB-backed counter
228
+		// state, segregated by Policy.Scope.
229
+		searchH, err := buildSearchHandlers(pool, deps.TemplatesFS, logger, ratelimit.New(pool))
225230
 		if err != nil {
226231
 			return fmt.Errorf("search handlers: %w", err)
227232
 		}