Skip to content

fix(rate-limit): SQL interval parameterization — Phase 3 hotfix#316

Merged
blove merged 1 commit into
mainfrom
claude/fix-rate-limit-interval-sql
May 14, 2026
Merged

fix(rate-limit): SQL interval parameterization — Phase 3 hotfix#316
blove merged 1 commit into
mainfrom
claude/fix-rate-limit-interval-sql

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 14, 2026

Summary

Hotfix for #315. The Phase 3 rate limit was a silent no-op in
production. Symptom: 12 streaming requests in a row from one IP all
returned 200; the `rate_limit_events` table stayed empty.

Root cause

The SQL used `interval '${WINDOW_SECONDS} seconds'` inside a
`@neondatabase/serverless` tagged template. The driver substitutes
`${...}` placeholders as `$N` parameters, but parameters cannot
appear inside a Postgres string literal. The driver emitted
`interval '$2 seconds'`, Postgres rejected it with
`invalid input syntax for type interval`, and the proxy's
fail-open catch allowed the request through silently.

Fix

Build `WINDOW_INTERVAL = '60 seconds'` at module load and splice it
as a parameterized value cast to `::interval`:

```ts
ts < now() - ${WINDOW_INTERVAL}::interval
```

That emits `ts < now() - $2::interval`, which Postgres evaluates
correctly. Also added the same window filter to the SELECT so
DELETE+SELECT share the same boundary.

Verification

Smoke-tested against the live Neon DB before pushing:

```
Request 1: count=1, allowed=true
...
Request 10: count=10, allowed=true
Request 11: count=11, allowed=false ← rate limited
Request 12: count=12, allowed=false
```

Test plan

  • Existing 5 rate-limit unit tests still pass (mocks unaffected)
  • Live SQL smoke-test against Neon confirms 1-10 allowed, 11+ denied
  • After merge: re-run the 12-request HTTP smoke against `demo.cacheplane.ai`

🤖 Generated with Claude Code

…lue, not inside a quoted literal

Phase 3 (#315) introduced a per-IP rate limit that was a silent
no-op in production. Symptom: 12 streaming requests in a row all
returned 200; rate_limit_events table had 0 rows.

Root cause: the SQL used `interval '${WINDOW_SECONDS} seconds'`
inside a tagged-template literal. The @neondatabase/serverless
driver substitutes `${...}` placeholders as $N parameters, but
parameters cannot appear inside a Postgres string literal. The
driver emitted `interval '$2 seconds'` and the planner rejected it
with `invalid input syntax for type interval`. The proxy's
fail-open catch then allowed the request through.

Fix: build `WINDOW_INTERVAL = '60 seconds'` at module load and
splice it as a parameterized value cast to ::interval:
  `ts < now() - ${WINDOW_INTERVAL}::interval`
That emits `ts < now() - $2::interval`, which Postgres evaluates
correctly.

Also added `AND ts > now() - ${WINDOW_INTERVAL}::interval` to the
SELECT — the DELETE+SELECT now use the same window boundary so the
count can't accidentally include rows that the DELETE didn't yet
prune.

Smoke-tested against the live Neon DB:
  Request 1: count=1, allowed=true
  ...
  Request 10: count=10, allowed=true
  Request 11: count=11, allowed=false  ← rate limited
  Request 12: count=12, allowed=false

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cacheplane Ready Ready Preview, Comment May 14, 2026 6:24am

Request Review

@blove blove merged commit 0f14912 into main May 14, 2026
16 checks passed
blove added a commit that referenced this pull request May 14, 2026
The Phase 3 hotfix in #316 fixed a SQL bug in scripts/rate-limit.ts
but never reached production: the CI Check if demo changed step's
regex only watched assemble-demo, demo-middleware, and langgraph-proxy.
A rate-limit.ts change alone evaluated demo_changed=false, the deploy
step was skipped, and demo.cacheplane.ai kept serving the original
broken bundle.

Two changes:

1. Extend the watched regex to include scripts/rate-limit.ts.
2. Touch scripts/demo-middleware.ts (a documentation comment) so
   THIS PR matches the existing watched paths and triggers the
   redeploy that picks up #316's SQL fix.

Also documents the gating contract in demo-middleware.ts itself so a
future engineer splitting rate-limit into multiple files knows to
update the regex.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
blove added a commit that referenced this pull request May 14, 2026
The Phase 3 hotfix in #316 fixed a SQL bug in scripts/rate-limit.ts
but never reached production: the CI Check if demo changed step's
regex only watched assemble-demo, demo-middleware, and langgraph-proxy.
A rate-limit.ts change alone evaluated demo_changed=false, the deploy
step was skipped, and demo.cacheplane.ai kept serving the original
broken bundle.

Two changes:

1. Extend the watched regex to include scripts/rate-limit.ts.
2. Touch scripts/demo-middleware.ts (a documentation comment) so
   THIS PR matches the existing watched paths and triggers the
   redeploy that picks up #316's SQL fix.

Also documents the gating contract in demo-middleware.ts itself so a
future engineer splitting rate-limit into multiple files knows to
update the regex.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant