GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat(server): add rate limiting middleware by JosunLP · Pull Request #240 · bQuery/bQuery

Conversation

@JosunLP

@JosunLP JosunLP commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds rateLimit() to @bquery/bquery/server — fixed-window throttling with RateLimit-* headers, 429 + Retry-After, and counters in a pluggable SessionStore.
  • keyBy is required rather than defaulting to the client address. That is a deliberate departure from the issue; the reasoning is below and I'd like a second opinion on it.

Motivation

Closes #223.

No server-side throttling primitive existed, so any public endpoint was unprotected against brute force by default — including the login route in the docs' own login-form cookbook recipe, which this PR also fixes.

Changes included

  • New src/server/rate-limit.ts — rateLimit() plus consume(), forwardedAddress() and withRateLimitHeaders(), exported for testing.
  • src/server/index.ts, src/full.ts — exports.
  • docs/guide/server.md — a "Rate limiting" section placed directly alongside basicAuth/bearerAuth, as the issue asked, with a keyBy selection table and an options table.
  • docs/cookbook/login-form.md — the recipe now shows the rate-limited server route.
  • New tests/server-rate-limit.test.ts — 35 tests.
  • CHANGELOG entry.

Design, as the issue specified

  • Counters use SessionStore — the same abstraction sessions use, so a Redis-backed store plugs in the same way and the limit holds across processes. Keys are prefixed (rl: by default) so they cannot collide with sessions.
  • Standard headers — RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset on every response; Retry-After on the 429.
  • 429 with Retry-After, overridable via status/message/onLimit.
  • Paired with the auth documentation.

Plus skip, skipSuccessfulRequests (so a valid login does not spend budget) and headers: false.

Validation

  • bun run lint — bunx eslint . clean
  • bun run build
  • bun test — 3102 pass, 1 fail (the pre-existing [Bug]: IPv6 server test fails instead of skipping in IPv6-less environments #213 IPv6 test, fixed in fix(tests): skip the IPv6 listen test when the runtime has no IPv6 loopback #232)
  • Additional focused checks: bun run check (26/26 server exports documented), format:check, lint:types pass

35 new tests, weighted toward the boundary: the max-th request must pass and the next must not, per-key isolation, window rollover, refunds, shared stores, and the configuration errors.

Checklist

  • I followed the contributing guidelines.
  • I added or updated tests where behavior changed.
  • I updated documentation when public behavior or developer workflow changed.
  • I confirmed the change does not introduce security-sensitive regressions.

Notes for reviewers

The one place I did not follow the issue

The issue says "default keyBy to the client address, overridable to key by session or user id." ServerContext exposes no peer address — there is no ctx.address, and nothing in the Bun/Node/Deno adapters plumbs one through. So the only available "client address" is X-Forwarded-For, which the client sets unless a proxy you control overwrites it.

Defaulting to it would ship a limiter that an attacker bypasses by sending a different header per request — worse than no limiter, because the app looks protected. So:

  • keyBy is required, and the constructor throws a TypeError naming both ways forward when it is missing.
  • trustProxy: true opts into the forwarded-header behaviour explicitly, for deployments where a proxy overwrites it.

The better fix is plumbing a real ctx.address through the three runtime adapters (server.requestIP() on Bun, req.socket.remoteAddress on Node, info.remoteAddr on Deno). That is a core change touching all three adapters and app.handle() has no address at all, so I kept it out of this PR — happy to open an issue, or to add it here if you'd rather.

Two smaller notes

  • remaining is clamped at zero, so the over-limit check compares the raw count. My first version compared limit - remaining > max, which silently let every request past the limit through, because both the max-th and the 500th request report remaining: 0. RateLimitState now carries an unclamped count, and the boundary tests pin it.
  • The window is fixed, not sliding. This allows a burst of up to 2 × max across a boundary — the standard trade-off, documented in the guide. A sliding window needs per-request timestamps in the store, a much larger write cost for a limiter whose job is to be cheap.
  • The default memoryStore() only limits one process, which the guide calls out in a warning, since a load-balanced deployment would otherwise get N × max.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@JosunLP @claude
feat(server): add rate limiting middleware
49fc8b3
No throttling primitive existed server-side, so any public endpoint was
unprotected against brute force by default — including the login route in
the docs' own `login-form` recipe, which now shows the limit.
(`createRequestQueue` in `reactive` is the client-side mirror image: it
limits outgoing parallel requests, not incoming ones.)

`rateLimit({ window, max, keyBy, store, ... })` counts requests per key in
a fixed window. Counters live in a `SessionStore`, the same abstraction
sessions use, so a Redis-backed store plugs in the same way and the limit
holds across processes. Responses carry `RateLimit-Limit`, `-Remaining`
and `-Reset`; a rejected request gets 429 with `Retry-After`.

`keyBy` is required rather than defaulting to the client address, which
is a deliberate departure from the issue. `ServerContext` exposes no peer
address, so the only available default would be `X-Forwarded-For` — a
header the client sets unless a proxy you control overwrites it. Defaulting
to it would ship a limiter an attacker bypasses by varying the header per
request: worse than none, because it looks like protection. `trustProxy:
true` opts into that behaviour explicitly, and the constructor throws with
a message naming both options when neither is given.

The over-limit test compares the raw count rather than `remaining`, which
is clamped at zero and so cannot tell the max-th request from the one
after it — the boundary tests cover both.

Closes #223

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5d35f86f-e3b5-4bc9-9fcd-15abeb9eb62b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
  • X
  • Mastodon
  • Reddit
  • LinkedIn

Comment @coderabbitai help to get the list of available commands.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions github-actions Bot added docs Changes to the documentation tests Chenges to the tests server labels Sep 18, 2026
@JosunLP
Merge branch 'dev' into feat/223-rate-limit
6240ff8
@JosunLP
JosunLP marked this pull request as ready for review September 20, 2026 12:54
JosunLP
JosunLP commented Sep 20, 2026
View reviewed changes

@JosunLP JosunLP left a comment

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Code review of the rate-limiting middleware. The structure, the SessionStore reuse and the count-vs-remaining boundary reasoning are all sound, and the analysis in the PR description about not defaulting keyBy to a spoofable header is the right call. But three findings mean the limiter does not hold in the cases it was added for — each reproduced against this branch:

  1. The documented login recipe applies no limit at all. ctx.session?.$id is null until a session is written, so a cookie-less brute force keys to null and every request is skipped. 20/20 attempts reach the handler. This is the exact scenario the PR sets out to fix, in the guide, the cookbook and the JSDoc.
  2. consume() is a non-atomic read-modify-write, so concurrent requests on one key are undercounted: 10 parallel requests with max: 2 all returned 200, with the default synchronous memoryStore. Parallel connections are the normal shape of the attack being defended against.
  3. trustProxy trusts the leftmost X-Forwarded-For ahead of CF-Connecting-IP/True-Client-IP, but Cloudflare and nginx's standard config append to XFF rather than overwrite, so the leftmost entry stays client-controlled and the opt-in path is bypassed by rotating the header — 5/5 requests passed with max: 1.

Plus: the default memoryStore() is unbounded and never sweeps expired keys (memory exhaustion via key rotation), skipSuccessfulRequests refunds 3xx and so disables the limit on a redirect-on-failure login form, the refund does not check that the window is still the one it counted in, and headers: false silently drops Retry-After from the 429.

Detail and repros in the inline comments.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread docs/guide/server.md Outdated
rateLimit({
window: 15 * 60_000,
max: 5,
keyBy: (ctx) => ctx.session?.$id ?? null,

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

The headline example applies no limit at all to the attack it is meant to stop.

ctx.session.$id is null until the session is first written to (createSessionController: id = initialId from the cookie, and ensureId() only runs on markDirty()). A password-guessing script sends no session cookie, so keyBy returns null on every attempt, and the middleware does if (identity === null ...) return next() — the limit is skipped entirely. Even a client that does have a session simply drops the cookie.

Verified against this branch:

app.use(session({ secret }));
app.post('/login', handler, [rateLimit({ window: 15*60_000, max: 5, keyBy: (ctx) => ctx.session?.$id ?? null, skipSuccessfulRequests: true })]);
// 20 cookie-less POSTs → 20 reach the handler, 0 × 429

The same shape recurs in the keyBy table below (line 404): ctx.state.user?.id ?? null is null for exactly the unauthenticated attacker, and headers.get('x-api-key') is null for a request that omits the header. Every recommended key fails open for the caller you most want to throttle.

Worth reconsidering both the docs and the API: a null key meaning "unlimited" is a footgun for a security primitive. Options are a documented fallback bucket (e.g. ctx.session?.$id ?? 'anon' plus a second discriminator), or making the skip explicit (false skips, null is an error) so it cannot happen by accident.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread docs/cookbook/login-form.md Outdated
rateLimit({
window: 15 * 60_000,
max: 5,
keyBy: (ctx) => ctx.session?.$id ?? null,

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Same problem as the guide example: ctx.session?.$id is null for a client that sends no session cookie, and a null key skips the limit. The recipe this PR describes as previously "unprotected against brute force" is still unprotected after the change — a script that just never sends a cookie gets unlimited attempts (verified: 20/20 POSTs reach handleLogin).


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/server/rate-limit.ts
window: number,
now: number
): Promise<RateLimitState> => {
const existing = await store.get(key);

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

consume() is a read-modify-write with an await in the middle, so concurrent requests on the same key all read the same counter and the limit does not hold.

Every in-flight request for one key that reaches await store.get(key) before any of them reaches store.set reads the same count and writes count + 1, so N concurrent requests advance the counter by 1. This is not theoretical or specific to async stores — the default memoryStore() is synchronous and it still happens, because await on a non-promise yields to the microtask queue:

app.get('/c', handler, [rateLimit({ window: 60_000, max: 2, keyBy: () => 'k' })]);
await Promise.all(Array.from({ length: 10 }, () => app.handle('/c')));
// → 200,200,200,200,200,200,200,200,200,200  (expected 2 × 200, 8 × 429)

Ten simultaneous connections is what a brute-force or credential-stuffing tool does by default, so the limiter can be bypassed outright rather than merely overshot. With a Redis-backed SessionStore the window is wider still (two network round trips per request).

SessionStore has no atomic increment, so a full fix needs either an optional incr-style hook on the store or per-key serialization. Even a process-local in-flight promise chain keyed by key (chain = chain.then(() => doConsume())) would close the single-process case that the default store and the docs' own login recipe run in. Whatever the fix, this deserves a test using Promise.all — the current suite only ever awaits requests one at a time, which is why it passes.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/server/rate-limit.ts Outdated
import type { ServerContext, ServerHandler, ServerMiddleware } from './types';

/** Headers a proxy may use to report the originating client, in priority order. */
const FORWARDED_HEADERS = ['x-forwarded-for', 'x-real-ip', 'cf-connecting-ip', 'true-client-ip'];

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

The header priority makes trustProxy: true spoofable in exactly the deployments it is meant for.

x-forwarded-for is checked first and the leftmost entry is taken. But the common proxies append rather than overwrite: Cloudflare appends the connecting IP to a client-supplied X-Forwarded-For (and reports the real one in CF-Connecting-IP), and nginx's standard proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for does the same. So behind such a proxy the leftmost XFF entry is attacker-controlled, and it wins over the trustworthy cf-connecting-ip / true-client-ip that this list itself anticipates:

app.get('/p', handler, [rateLimit({ window: 60_000, max: 1, trustProxy: true })]);
// 5 requests, each with x-forwarded-for: 9.9.9.<i> and a fixed cf-connecting-ip: 7.7.7.7
// → 200,200,200,200,200

That is the exact failure mode the PR description argues against ("a limiter an attacker bypasses by sending a different header per request"), still present in the opt-in path. Suggest ordering the single-value, proxy-set headers first (cf-connecting-ip, true-client-ip, x-real-ip) and only then x-forwarded-for, and/or letting the caller name the header to trust (plus a hop count for XFF, taking the Nth entry from the right) rather than a fixed preference list. The guide's wording ("Behind a proxy that overwrites X-Forwarded-For") should also be sharper, since the two most widely deployed proxies do not overwrite it.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/server/rate-limit.ts
if (skip && (await skip(ctx))) return next();

const identity = await resolveKey(ctx);
if (identity === null || identity === undefined || identity === '') return next();

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

With trustProxy, a request carrying none of the forwarding headers is not limited at all — forwardedAddress() returns null and this line skips straight to next(). The test at tests/server-rate-limit.test.ts:248 pins that as intended ("skips ... rather than lumping them together"), but the consequence is a fail-open: any request that reaches the origin without passing through the proxy (origin port exposed alongside the CDN, a health-check path, an internal hop, a proxy misconfigured after a deploy) bypasses the limiter entirely, and the app still reports itself as protected.

For a keyBy the caller wrote, "return null to skip" is a deliberate choice; for the built-in trustProxy path the safer default is to fail closed — count header-less requests under one shared bucket (they are, by definition, not distinguishable) or reject them — with skipping available via skip if someone really wants it.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/server/rate-limit.ts Outdated
max,
keyBy,
trustProxy = false,
store = memoryStore(),

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

The default memoryStore() grows without bound under the traffic a limiter exists to absorb.

memoryStore() here is constructed with no maxEntries, and it only ever drops an expired entry lazily, inside get(id) for that same id (src/server/session.ts:105-115); there is no sweep. A rate-limit key is attacker-chosen (a forwarded address, an API key, a user id) and typically never seen twice, so each distinct key leaves a Map entry that is expired but never collected — one entry per unique key, for the lifetime of the process. An attacker rotating X-Forwarded-For (or x-api-key) turns the limiter into a memory-exhaustion vector, which is worse than the request flood it was added to stop.

memoryStore({ maxEntries }) already exists; defaulting to a bounded store (with the cap documented) would fix it. Note also the eviction is insertion-order, so if someone follows the guide's advice to pass the session store here, rate-limit churn will evict live sessions and log users out — worth either a warning in the guide or keeping the default counters in their own store.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/server/rate-limit.ts Outdated

// Refunding a successful request keeps a burst of valid traffic from
// locking a user out, while failures still count toward the limit.
if (skipSuccessfulRequests && response.status < 400) {

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

status < 400 refunds redirects, which silently disables the limit on the most common login-form shape. The POST/redirect/GET pattern the login-form recipe is about answers a failed login with 302 /login?error=1, so every failed attempt is refunded:

app.post('/login', (ctx) => ctx.redirect('/login?error=1', 302), [
  rateLimit({ window: 60_000, max: 2, keyBy: () => 'k', skipSuccessfulRequests: true }),
]);
// 8 attempts → 302 × 8, never a 429

skipSuccessfulRequests is a security-relevant switch, so "success" should mean what the route means by it — consider a predicate (skipSuccessful?: (res) => boolean) or at least restricting the default to 2xx and documenting the redirect trap in the guide's options table.

Two further issues in this block:

  • The refund never checks that the stored record is still the window the request was counted in. If the window rolls over while the handler runs (slow password hash near a boundary), current is the new window's counter and the decrement steals a request from it — that window then allows max + 1. Guarding with current.resetAt === state.resetAt fixes it.
  • The get + set here is the same non-atomic read-modify-write as consume(), so concurrent refunds on one key lose decrements (or, combined with a concurrent consume, lose increments).

Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/server/rate-limit.ts Outdated
// so it cannot tell the `max`-th request from the one after it.
if (state.count > max) {
const denied = onLimit ? await onLimit(ctx) : ctx.text(message, { status });
return emitHeaders ? withRateLimitHeaders(denied, state, true) : denied;

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

headers: false also drops Retry-After from the 429, which the option does not advertise — it is documented as "Emit RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset", and the guide states unconditionally that an over-limit request "is answered 429 Too Many Requests with Retry-After". Verified: with headers: false, the 429 has retry-after: null, so a client has nothing to back off on.

Retry-After is a standard part of a 429 rather than one of the RateLimit-* informational headers; suggest always applying it to the rejection and letting headers govern only the three RateLimit-* ones.

Suggested change
return emitHeaders ? withRateLimitHeaders(denied, state, true) : denied;
const denied = onLimit ? await onLimit(ctx) : ctx.text(message, { status });
return withRateLimitHeaders(denied, state, true, emitHeaders);

(with withRateLimitHeaders taking a flag for the RateLimit-* trio, or a small helper that sets only retry-after.)


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/server/rate-limit.ts Outdated
* const loginLimit = rateLimit({
* window: 15 * 60_000,
* max: 5,
* keyBy: (ctx) => ctx.session?.$id ?? null,

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

This @example has the same defect as the guide and cookbook copies: on a login route the caller has no session yet, $id is null until the session is first written, and a null key skips the limit — so the documented way to "protect a login route" protects nothing. Worth replacing with a key that exists for an unauthenticated request (e.g. the submitted username plus a coarse bucket, or trustProxy behind a proxy) once the null-skips-everything semantics are settled.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
claude and others added 3 commits September 20, 2026 18:26
@claude
fix(server): make the rate limiter hold in the cases it was added for
2694108
Review findings on #223, each reproduced before the fix:

- The documented login recipe applied no limit at all. `ctx.session?.$id` is
  `null` until a session is written, and a `null` key skips the limit, so a
  cookie-less brute force was unthrottled: 20/20 attempts reached the
  handler. Guide, cookbook and JSDoc now key on the proxy-reported address
  (or a counted `?? 'anon'` bucket), and `keyBy` documents that `null` fails
  open.
- `consume()` is a read-modify-write across an `await`, so 10 parallel
  requests with `max: 2` all returned 200 — with the synchronous default
  store. Calls are now serialized per key; the new test fails without it.
- `trustProxy` preferred the leftmost `X-Forwarded-For`, which Cloudflare and
  stock nginx *append* to, leaving it client-controlled. The single-value
  proxy headers now win, so rotating XFF no longer bypasses the limit.
- A request reaching the origin with no forwarding header was skipped
  entirely. It is now counted in one shared bucket: off-proxy traffic must
  not be unlimited while the app reports itself protected.
- The default store was unbounded and swept nothing, so rotated keys grew it
  without limit. It is now capped, and the guide warns against passing the
  session store.
- `skipSuccessfulRequests` refunded any status below 400, so a
  redirect-on-failure login form was refunded on every failed attempt. Only
  2xx is refunded now, and only when the window has not rolled over — the
  old refund could steal budget from the next window.
- `headers: false` also dropped `Retry-After` from the 429, contradicting
  both the option doc and the guide. It is now always sent on a rejection.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@claude
Merge origin/dev into feat/223-rate-limit
aa6416d
Resolves four conflicts, all of them the same shape: #222's
`serveStatic()` and #223's `rateLimit()` land side by side in the
server barrel, the `full` re-export list, the server guide's API
table and the changelog.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@JosunLP
Merge branch 'dev' into feat/223-rate-limit
ca99a43
@JosunLP
JosunLP merged commit 352cea1 into dev Sep 21, 2026
25 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@JosunLP
JosunLP deleted the feat/223-rate-limit branch September 21, 2026 08:08
@JosunLP JosunLP mentioned this pull request Sep 21, 2026
[Feature]: Rate limiting middleware for server #223
Closed
JosunLP added a commit that referenced this pull request Sep 21, 2026
@JosunLP @claude
feat(server): add rate limiting middleware (#240)
52518f6
Co-authored-by: Claude Opus 5 <[email protected]>
Closes #223
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

docs Changes to the documentation server tests Chenges to the tests

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@JosunLP @claude
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.