Skip to content

Bump search bucket to 240/min and add user repos/starred passthrough#4

Merged
rainxchzed merged 1 commit into
mainfrom
higher-bucket-and-user-repos
May 4, 2026
Merged

Bump search bucket to 240/min and add user repos/starred passthrough#4
rainxchzed merged 1 commit into
mainfrom
higher-bucket-and-user-repos

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 4, 2026

Summary

Two changes addressing the rate-limit prompts seen in the client app, plus two new endpoints for the developer-profile screen.

1. Bump search bucket: 60 → 240 / min

Plugins.kt — the bucket covering /search, /search/explore, /releases, /readme, /user, /users/{u}/repos, /users/{u}/starred was sized at 60/min when the client made far fewer parallel calls per session. With 90k users, a single details-page open fans out to 3-4 of these endpoints, and the developer-profile screen pulls 2 more.

Sizing rationale:

  • Aggregate pool quota across 4 PATs: ~20k/hr to GitHub. Bucket-rate-limit was the constraint, not the upstream.
  • Cloudflare s-maxage on /repo (300s) and tiered cache absorb most repeat reads anyway.
  • Token-hash keying still segments per-user (a single user can't burn another's slot).

2. New passthrough endpoints

  • GET /v1/users/{username}/repos?type=owner&sort=updated&direction=desc&page=N&per_page=N
  • GET /v1/users/{username}/starred?sort=created&direction=desc&page=N&per_page=N

Both follow the existing GitHubResourceClient.fetchCached pattern (edge cache headers, stale-while-revalidate fallback, negative-hit caching, ETag pass-through). Query params are whitelisted to block SSRF via injection.

Cache shape:

Route Server TTL max-age s-maxage
/users/{u}/repos 1h 300 1800
/users/{u}/starred 30min 180 900

The OAuth /user/starred (signed-in viewer's own stars) is NOT proxied — that form requires per-user-session caching that doesn't fit this passthrough model.

Out of scope (deferred)

  • releases-with-apk-only pre-filter — premature; reconsider after the picker scan ships and we see real fan-out.
  • ASN / CGNAT bypass — real fix is a client-supplied installation-id keyed alongside IP for anon traffic. Separate PR.
  • Surfacing rate-limit headers — already exposed by Ktor's RateLimit plugin (X-RateLimit-Limit/Remaining/Reset); only client-side UX work remains.

Test plan

  • ./gradlew test green (no behaviour change to existing routes; bucket size is config).
  • After merge + auto-deploy, hit each new endpoint with curl -I and confirm 200 + the documented Cache-Control values.
  • Watch for any spike in 429s on the search bucket — should drop substantially as headroom is now 4x.

Summary by CodeRabbit

  • New Features

    • Added GET /v1/users/{username}/repos endpoint to retrieve a user's repositories with configurable pagination and sorting.
    • Added GET /v1/users/{username}/starred endpoint to retrieve a user's starred repositories with configurable pagination and sorting.
    • Implemented server-side caching for both endpoints to improve performance.
  • Performance

    • Increased search operation rate limit from 60 to 240 requests per minute.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

Two new GitHub-proxy endpoints were added: GET /v1/users/{username}/repos and GET /v1/users/{username}/starred. Both validate query parameters, construct cache keys, delegate to GitHubResourceClient.fetchCached with configurable TTLs, and map response states to appropriate HTTP headers and status codes. The search rate-limit bucket was increased from 60 to 240 requests/min to accommodate the new routes.

Changes

GitHub User Repositories and Starred Endpoints

Layer / File(s) Summary
API Documentation
CLAUDE.md
Two new endpoints documented: /users/{username}/repos (with type, sort, direction, paging, SSRF whitelisting note) and /users/{username}/starred (with sort, direction, paging; notes explicit exclusion of OAuth viewer-self form).
Parameter Validation & Whitelists
src/main/kotlin/zed/rainxch/githubstore/routes/UserReposRoutes.kt, src/main/kotlin/zed/rainxch/githubstore/routes/UserStarredRoutes.kt
Both routes define whitelisted values (VALID_TYPES, VALID_SORTS, VALID_DIRECTIONS) and validate username via GitHubIdentifiers.validOwner; page and per_page are clamped to safe bounds.
Core Route Handlers
src/main/kotlin/zed/rainxch/githubstore/routes/UserReposRoutes.kt, src/main/kotlin/zed/rainxch/githubstore/routes/UserStarredRoutes.kt
userReposRoutes maps GET /v1/users/{username}/repos with 1-hour TTL; userStarredRoutes maps GET /v1/users/{username}/starred with 30-minute TTL. Both extract optional X-GitHub-Token, construct cache keys, and delegate to GitHubResourceClient.fetchCached. Cache result handling branches on hit (public cache headers), stale fallback (no-store + X-Cache-State), negative hit (error payload), and upstream error (502).
Route Registration
src/main/kotlin/zed/rainxch/githubstore/routes/Routing.kt
New route handlers userReposRoutes and userStarredRoutes registered under the /v1 "search" rate-limited scope.
Rate Limit Configuration
src/main/kotlin/zed/rainxch/githubstore/Plugins.kt
Search bucket rate limit increased from 60 to 240 requests/min; documentation expanded to describe the new upstream-passthrough routes and bump rationale.

Sequence Diagram

sequenceDiagram
    participant Client
    participant RouteHandler as Route Handler<br/>(userReposRoutes)
    participant Cache as GitHubResourceClient<br/>(fetchCached)
    participant GitHub as GitHub API

    Client->>RouteHandler: GET /v1/users/{user}/repos?sort=updated&page=1
    activate RouteHandler
    
    Note over RouteHandler: Validate username,<br/>whitelist params,<br/>build cache key
    
    RouteHandler->>Cache: fetchCached(cacheKey, upstreamURL, token?, 1h TTL)
    activate Cache
    
    alt Cache Hit
        Cache-->>RouteHandler: Result.Hit(cachedBody)
        RouteHandler-->>Client: 200 OK + Cache-Control: public
    else Cache Miss
        Cache->>GitHub: GET /users/{user}/repos (with optional token)
        activate GitHub
        GitHub-->>Cache: 200 + JSON
        deactivate GitHub
        Cache-->>RouteHandler: Result.StaleFallback(body)
        RouteHandler-->>Client: 200 OK + X-Cache-State: stale-fallback
    else Upstream Error
        Cache->>GitHub: GET /users/{user}/repos
        activate GitHub
        GitHub-->>Cache: 5xx Error
        deactivate GitHub
        Cache-->>RouteHandler: Result.UpstreamError
        RouteHandler-->>Client: 502 Bad Gateway + github_unreachable
    end
    
    deactivate Cache
    deactivate RouteHandler
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Two repos now shine, starred and listed with care,
Cache keeps them speedy, with tokens to spare,
GitHub's grand halls now accessible this way,
Hopping through endpoints, we fetch and we slay!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: increasing the search bucket limit to 240/min and adding two new GitHub passthrough endpoints for user repos and starred repositories.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch higher-bucket-and-user-repos

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/kotlin/zed/rainxch/githubstore/Plugins.kt (1)

71-78: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale function-level comment after rate-limit bump.

The searchBucketKey docblock has two stale references following the 60→240 bump and the addition of the new routes:

  • Lines 71–72: the route list still reads /search, /search/explore, /releases, /readme, /user — the newly added /users/{u}/repos and /users/{u}/starred are missing.
  • Line 74: "a single 60/min slot" — should now read 240/min to match the actual limit.

The register() block comment at lines 170–173 was updated correctly; the function-level companion was not.

✏️ Proposed fix
-// Key function for the search bucket (covers /search, /search/explore,
-// /releases, /readme, /user — all the upstream-passthrough routes). Behind a
-// CDN POP, IP-only keying collapses every user behind one regional POP into
-// a single 60/min slot. Keying by a hash of the user's X-GitHub-Token when
+// Key function for the search bucket (covers /search, /search/explore,
+// /releases, /readme, /user, /users/{u}/repos, /users/{u}/starred —
+// all the upstream-passthrough routes). Behind a CDN POP, IP-only keying
+// collapses every user behind one regional POP into a single 240/min slot.
+// Keying by a hash of the user's X-GitHub-Token when
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/zed/rainxch/githubstore/Plugins.kt` around lines 71 - 78,
Update the stale docblock for the searchBucketKey function: change the rate text
from "a single 60/min slot" to "240/min" and add the newly supported routes
(`/users/{u}/repos` and `/users/{u}/starred`) to the route list in the comment;
ensure the comment still mentions token-hash behavior (HmacSHA256 pepper
truncated to 16 hex chars) and that anonymous callers are keyed by IP. Reference
the searchBucketKey function and the existing register() comment to mirror the
corrected language.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/main/kotlin/zed/rainxch/githubstore/Plugins.kt`:
- Around line 71-78: Update the stale docblock for the searchBucketKey function:
change the rate text from "a single 60/min slot" to "240/min" and add the newly
supported routes (`/users/{u}/repos` and `/users/{u}/starred`) to the route list
in the comment; ensure the comment still mentions token-hash behavior
(HmacSHA256 pepper truncated to 16 hex chars) and that anonymous callers are
keyed by IP. Reference the searchBucketKey function and the existing register()
comment to mirror the corrected language.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0b9316ea-ad50-4462-8ebc-f91dbec75cce

📥 Commits

Reviewing files that changed from the base of the PR and between a3d2128 and 232731b.

📒 Files selected for processing (5)
  • CLAUDE.md
  • src/main/kotlin/zed/rainxch/githubstore/Plugins.kt
  • src/main/kotlin/zed/rainxch/githubstore/routes/Routing.kt
  • src/main/kotlin/zed/rainxch/githubstore/routes/UserReposRoutes.kt
  • src/main/kotlin/zed/rainxch/githubstore/routes/UserStarredRoutes.kt

@rainxchzed rainxchzed merged commit dc9a736 into main May 4, 2026
2 checks passed
@rainxchzed rainxchzed deleted the higher-bucket-and-user-repos branch May 4, 2026 12:10
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