Skip to content

Commit 0b13e4c

Browse files
committed
fix(web): rate-limit /api/docs/search
The route had zero throttling beyond a 128-char query cap, unlike every other demo endpoint. A scripted client could hit it at unlimited req/s from one IP. Reuse the existing in-memory clientIp/checkWebhookCooldown pair from lib/demo-limits.ts, same pattern as /api/webhook-sample. SearchDialog cast res.json() straight to SearchResult[] with no res.ok check, so a 429 envelope would have been treated as a result array and broken rendering - fixed alongside the rate limit that would trigger it.
1 parent b972e76 commit 0b13e4c

2 files changed

Lines changed: 15 additions & 0 deletions

File tree

apps/web/app/api/docs/search/route.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'fs'
33
import path from 'path'
44
import matter from 'gray-matter'
55
import { docSections } from '@/lib/docroutes'
6+
import { checkWebhookCooldown, clientIp } from '@/lib/demo-limits'
67

78
export type SearchResult = {
89
title: string
@@ -100,6 +101,15 @@ function getCorpus(): IndexedDoc[] {
100101
const MAX_QUERY_LENGTH = 128
101102

102103
export async function GET(request: NextRequest) {
104+
const ip = clientIp(request)
105+
const cooldown = checkWebhookCooldown(ip)
106+
if (!cooldown.ok) {
107+
return NextResponse.json(cooldown.body, {
108+
status: 429,
109+
headers: { 'Retry-After': String(Math.ceil(cooldown.body.retryAfterMs / 1000)) },
110+
})
111+
}
112+
103113
const query = request.nextUrl.searchParams.get('q')?.trim().slice(0, MAX_QUERY_LENGTH) ?? ''
104114

105115
if (query.length < 2) {

apps/web/components/docs/SearchDialog.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export default function SearchDialog({ open, onClose }: Props) {
6666
debounceRef.current = setTimeout(async () => {
6767
try {
6868
const res = await fetch(`/api/docs/search?q=${encodeURIComponent(query)}`)
69+
if (!res.ok) {
70+
setResults([])
71+
setSelected(0)
72+
return
73+
}
6974
const data: SearchResult[] = await res.json()
7075
setResults(data)
7176
setSelected(0)

0 commit comments

Comments
 (0)