Skip to content

fix: add RPC timeout to fetchSlab/getSlot to prevent indefinite hangs#153

Open
0x-SquidSol wants to merge 1 commit intodcccrypto:mainfrom
0x-SquidSol:fix/rpc-timeout-fetchslab
Open

fix: add RPC timeout to fetchSlab/getSlot to prevent indefinite hangs#153
0x-SquidSol wants to merge 1 commit intodcccrypto:mainfrom
0x-SquidSol:fix/rpc-timeout-fetchslab

Conversation

@0x-SquidSol
Copy link
Copy Markdown
Contributor

@0x-SquidSol 0x-SquidSol commented Apr 9, 2026

Summary

  • Bug: fetchSlab() from @percolator/sdk and getConnection().getSlot() from @percolator/shared have no timeout. If the Solana RPC node is slow or unresponsive, requests to /markets/:slab, /api/adl/rankings, and /health hang indefinitely — tying up server connections with no upper bound.
  • Fix: Adds a shared withRpcTimeout() utility using Promise.race that wraps these opaque SDK calls with a configurable deadline.

Changes

File Change
src/utils/rpc-timeout.ts New utility — withRpcTimeout(), RpcTimeoutError class, env-configurable defaults
src/routes/markets.ts Wraps fetchSlab() at L126 with 10s timeout, returns 504 on timeout
src/routes/adl.ts Wraps fetchSlab() at L176 with 10s timeout, returns 504 on timeout
src/routes/health.ts Wraps getSlot() at L29 with 5s timeout, reports checks.rpc=false

Design decisions

  • Promise.race (not AbortSignal) — fetchSlab and getSlot don't accept signals
  • 10s route / 5s health — generous enough for normal RPC spikes, prevents indefinite hang
  • RPC_TIMEOUT_MS / HEALTH_RPC_TIMEOUT_MS env vars — tunable on Railway without redeploy
  • 504 Gateway Timeout — semantically correct for upstream RPC timeout
  • Custom RpcTimeoutError — enables instanceof checks and Sentry grouping

Test plan

  • tsc --noEmit passes (zero type errors)
  • vitest run passes (186/186 tests)
  • Manual: hit /markets/:slab with an unreachable RPC to verify 504 response
  • Verify Sentry groups RpcTimeoutError separately from generic 500s

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • RPC endpoint requests now enforce timeout protection.
    • Requests exceeding the timeout duration return HTTP 504 responses with error details.
    • Timeout durations are configurable per operation type.

…nite hangs

fetchSlab() from @percolator/sdk and getConnection().getSlot() from
@percolator/shared do not accept AbortSignal and have no internal
timeout. If the Solana RPC node is slow or unresponsive, requests to
/markets/:slab, /api/adl/rankings, and /health hang indefinitely.

This adds a shared withRpcTimeout() utility using Promise.race:
- Route endpoints (markets, adl): 10s default timeout, returns 504
- Health check: 5s timeout, reports checks.rpc=false on timeout
- Both values configurable via RPC_TIMEOUT_MS / HEALTH_RPC_TIMEOUT_MS env vars
- Custom RpcTimeoutError class for precise catch handling

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

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

A new RPC timeout utility is introduced to enforce bounded execution durations on RPC calls. The utility is applied to fetchSlab calls in adl.ts and markets.ts, and to getSlot in health.ts, with timeout errors returning HTTP 504 responses to clients.

Changes

Cohort / File(s) Summary
RPC Timeout Utility
src/utils/rpc-timeout.ts
New utility module providing configurable timeout constants (RPC_TIMEOUT_MS, HEALTH_RPC_TIMEOUT_MS), an RpcTimeoutError exception class, and a withRpcTimeout wrapper function that races promises against configurable timers.
RPC Timeout Integration
src/routes/adl.ts, src/routes/markets.ts
Wrapped fetchSlab RPC calls with withRpcTimeout, added catch handler to detect RpcTimeoutError and return HTTP 504 with { error: "Upstream RPC timeout", slab } response.
Health Check Timeout
src/routes/health.ts
Wrapped getConnection().getSlot() RPC call with withRpcTimeout using HEALTH_RPC_TIMEOUT_MS to enforce bounded duration for connectivity checks.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • dcccrypto

Poem

🐰 A timeout's ticking, races the race,
Promise meets timer in cyberspace,
RPC calls bounded, no hanging around,
504s when slowness is found! ⏱️✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding RPC timeout handling to fetchSlab and getSlot calls to prevent indefinite hangs, which is the core objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
src/utils/rpc-timeout.ts (1)

15-19: Harden timeout env parsing to avoid invalid runtime values.

Line 15 and Line 18 currently rely on Number(...) || fallback, which can silently accept problematic values (for example, negative/non-finite values). Prefer explicit finite/positive validation before using them as timeouts.

Suggested patch
 const DEFAULT_RPC_TIMEOUT_MS = 10_000;
 const DEFAULT_HEALTH_RPC_TIMEOUT_MS = 5_000;
 
-export const RPC_TIMEOUT_MS: number =
-  Number(process.env.RPC_TIMEOUT_MS) || DEFAULT_RPC_TIMEOUT_MS;
+function parseTimeoutMs(raw: string | undefined, fallback: number): number {
+  const parsed = Number(raw);
+  if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
+  return Math.floor(parsed);
+}
 
-export const HEALTH_RPC_TIMEOUT_MS: number =
-  Number(process.env.HEALTH_RPC_TIMEOUT_MS) || DEFAULT_HEALTH_RPC_TIMEOUT_MS;
+export const RPC_TIMEOUT_MS: number = parseTimeoutMs(
+  process.env.RPC_TIMEOUT_MS,
+  DEFAULT_RPC_TIMEOUT_MS,
+);
+
+export const HEALTH_RPC_TIMEOUT_MS: number = parseTimeoutMs(
+  process.env.HEALTH_RPC_TIMEOUT_MS,
+  DEFAULT_HEALTH_RPC_TIMEOUT_MS,
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/rpc-timeout.ts` around lines 15 - 19, The RPC_TIMEOUT_MS and
HEALTH_RPC_TIMEOUT_MS constants currently use Number(...) || fallback which
accepts invalid values; change them to explicitly parse and validate the env
values (e.g., use parseInt or Number, check Number.isFinite(value) and value >
0, optionally Math.floor to ensure an integer) and only use the parsed value
when it is a finite positive number, otherwise fall back to
DEFAULT_RPC_TIMEOUT_MS and DEFAULT_HEALTH_RPC_TIMEOUT_MS respectively; update
references to RPC_TIMEOUT_MS and HEALTH_RPC_TIMEOUT_MS in the file accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/utils/rpc-timeout.ts`:
- Around line 15-19: The RPC_TIMEOUT_MS and HEALTH_RPC_TIMEOUT_MS constants
currently use Number(...) || fallback which accepts invalid values; change them
to explicitly parse and validate the env values (e.g., use parseInt or Number,
check Number.isFinite(value) and value > 0, optionally Math.floor to ensure an
integer) and only use the parsed value when it is a finite positive number,
otherwise fall back to DEFAULT_RPC_TIMEOUT_MS and DEFAULT_HEALTH_RPC_TIMEOUT_MS
respectively; update references to RPC_TIMEOUT_MS and HEALTH_RPC_TIMEOUT_MS in
the file accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c7c5e74-9fb3-402f-bc2a-e9df38bd4f35

📥 Commits

Reviewing files that changed from the base of the PR and between fb94c29 and d26d370.

📒 Files selected for processing (4)
  • src/routes/adl.ts
  • src/routes/health.ts
  • src/routes/markets.ts
  • src/utils/rpc-timeout.ts

@0x-SquidSol
Copy link
Copy Markdown
Contributor Author

⚠️ This PR is superseded by #154

PR #154 is a superset of this one — it includes both the RPC timeout (withRpcTimeout) AND the RPC fallback (withRpcFallback) in a single cohesive change. Since both are open, only #154 needs to be merged.

Action: Safe to close in favor of #154. Note that #154 will need rebasing against current main due to the SDK migration (@percolator/sdk@percolatorct/sdk).

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