Skip to content

Add abuse protection to /api/query (closes #168) - #376

Open
SuhrudhC wants to merge 4 commits into
codeforpdx:mainfrom
SuhrudhC:feature-rate-limit-api-query
Open

Add abuse protection to /api/query (closes #168)#376
SuhrudhC wants to merge 4 commits into
codeforpdx:mainfrom
SuhrudhC:feature-rate-limit-api-query

Conversation

@SuhrudhC

Copy link
Copy Markdown

What type of PR is this? (check all applicable)

  • Bug Fix

Description

/api/query is the most expensive thing this app does — every request fans out to Vertex AI RAG plus a Gemini completion. Right now it has no rate limit at all, even though flask_limiter is already set up and used on /api/feedback. Anyone (or any script) can hit it as fast as they want, which is a real cost and availability risk.

This PR wires the existing limiter up to /api/query the same way it's already used on /api/feedback. It defaults to 10 requests per minute per IP, which should be plenty for a normal back-and-forth conversation but stops a script from hammering the endpoint. The limit is also configurable through a QUERY_RATE_LIMIT env var, so it can be tuned in production without a code change if it turns out to be too tight or too loose.

I also wrote a small script (backend/repro_rate_limit.py) that reproduces the bug by bursting both endpoints and comparing results — before the fix /api/query returned 200 for all 20 requests with zero rejections, and after the fix it correctly starts returning 429s past the 10th request.

Related Tickets & Documents

QA Instructions, Screenshots, Recordings

Easiest way to check this locally: send 11+ POST requests to /api/query from the same machine within a minute and confirm the 11th comes back with a 429. You can also just run uv run python backend/repro_rate_limit.py from the backend/ directory — it mocks out the LLM call so it doesn't need real GCP credentials, and prints out a before/after-style comparison of /api/query vs /api/feedback.

Added/updated tests?

  • Yes — two tests added to test_app.py (within-limit returns 200, over-limit returns 429), plus a new file test_query_rate_limit.py with 13 tests covering edge cases: the exact boundary (10th request succeeds, 11th doesn't), that two different IPs get separate quotas, that the window resets properly, that the env var is actually being read, and that /api/query and /api/feedback don't share counters with each other.

Documentation

  • No Architecture.md changes — the rate limiter itself isn't new, just where it's applied, so I didn't think this needed an architecture-level update. Happy to add a note if maintainers feel it's worth documenting.

[optional] Are there any post deployment tasks we need to perform?

Worth double-checking in production that the rate limiter is actually seeing real client IPs and not the IP of whatever proxy sits in front of the app (Digital Ocean). If it's only seeing the proxy's IP, every user would share one bucket. I didn't have access to verify the production networking setup, so flagging this for whoever reviews/deploys.

SuhrudhC and others added 3 commits June 14, 2026 20:27
Add a reproduction script and solution plan for the unprotected
/api/query endpoint. The script bursts both endpoints against the real
app with the LLM and email sender mocked: /api/query returns 200 for all
20 requests (no 429), while /api/feedback is throttled to 3/minute,
confirming the gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/api/query fans out to Vertex AI RAG plus a Gemini completion on every
call. Without a rate limit, scripted traffic is a direct cost and
availability risk. flask_limiter was already wired up but only applied to
/api/feedback.

Wrap the ChatView class-based view with limiter.limit() before
registering the route, defaulting to 10 per minute (tunable via
QUERY_RATE_LIMIT env var without a redeploy). Add two tests mirroring
the existing feedback rate-limit test: one confirming requests within the
budget return 200, one confirming the 11th request returns 429.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers six angles to confirm the fix in the previous commit holds:

  Boundary     — exactly 10 requests succeed, the 11th returns 429.
  Per-IP       — two source IPs each get their own independent quota.
  Window reset — limiter.reset() refills the window; traffic resumes.
  Env-var      — QUERY_RATE_LIMIT is read from os.environ at startup.
  Regression   — /api/feedback limit unchanged; the two endpoints have
                  independent counters in both directions.
  Response     — 429 reply has a non-empty HTML error body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread SOLUTION_PLAN.md Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you mean to check this in?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was a personal planning scratch file that got accidentally committed. I just deleted that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like this should go in the backend/scripts dir

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to backend/scripts/ alongside the other utility scripts. Let me know if there's anything else to fix before the pr can get merged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure you pass the code-quality checks. I enabled the github action checks, but you can run these checks locally as well (see the README.md).

- Delete SOLUTION_PLAN.md (internal scratch file, not intended for the repo)
- Move backend/repro_rate_limit.py → backend/scripts/ alongside other utility scripts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yangm2

yangm2 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@claude code-review

@github-actions

This comment was marked as resolved.

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.

Add abuse protection (rate limiting) to /api/query

2 participants