Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .agents/skills/ralph/prd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"project": "eth-rpc-proxy",
"branchName": "ralph/request-delay",
"description": "Request Delay Feature - Add configurable pre/post delays to simulate slow network conditions for testing",
"userStories": [
{
"id": "US-001",
"title": "Add pre-request delay via environment variable",
"description": "As a developer, I want to set a delay before requests are forwarded to upstream so that I can simulate slow network conditions.",
"acceptanceCriteria": [
"PROXY_PRE_DELAY_MS environment variable sets delay in milliseconds before forwarding",
"Delay is applied to both HTTP and WebSocket requests",
"Default value is 0 (no delay)",
"Delay value is logged at startup if set",
"Typecheck passes"
],
"priority": 1,
"passes": false,
"notes": ""
},
{
"id": "US-002",
"title": "Add post-response delay via environment variable",
"description": "As a developer, I want to set a delay after receiving upstream responses so that I can simulate slow response delivery.",
"acceptanceCriteria": [
"PROXY_POST_DELAY_MS environment variable sets delay in milliseconds before sending response to client",
"Delay is applied to both HTTP and WebSocket responses",
"Default value is 0 (no delay)",
"Delay value is logged at startup if set",
"Typecheck passes"
],
"priority": 2,
"passes": false,
"notes": ""
},
{
"id": "US-003",
"title": "Support programmatic delay configuration",
"description": "As a developer using the library, I want to configure delays programmatically so that I can set them in my test setup.",
"acceptanceCriteria": [
"setPreDelay(ms: number) method added to ProxyServer",
"setPostDelay(ms: number) method added to ProxyServer",
"Methods override environment variable values",
"Passing 0 disables the delay",
"Typecheck passes"
],
"priority": 3,
"passes": false,
"notes": ""
},
{
"id": "US-004",
"title": "Add delay tests",
"description": "As a developer, I want tests to verify delay behavior works correctly.",
"acceptanceCriteria": [
"Test verifies pre-delay adds expected latency to request",
"Test verifies post-delay adds expected latency to response",
"Test verifies combined delays work correctly",
"Tests pass with pnpm test"
],
"priority": 4,
"passes": false,
"notes": ""
}
]
}
40 changes: 39 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Useful for **testing, debugging, and simulating** RPC calls over different netwo
- 🎲 Deterministic or probabilistic (chaos) proxy behavior (forward / not answer / fail) allows observing/testing application behaviour
- 🔌 WebSocket + HTTP support
- 🧪 Designed for testing blockchain RPC clients
- ⏱️ Configurable pre-request and post-response delays for latency simulation

---

Expand Down Expand Up @@ -169,11 +170,48 @@ If no rule matches, the default mode/queue/probs apply.
| **Custom logic matcher** | `proxy.addRule((m) => m.startsWith("eth_") && !m.includes("send"), ProxyBehavior.NotAnswer)` | Flexible logic for targeting groups of methods. |
| **Fail once every 3 calls** | `proxy.addRule("eth_call", { mode: ProxyMode.Deterministic, behaviors: [ProxyBehavior.Fail, ProxyBehavior.Forward, ProxyBehavior.Forward] }); proxy.pushRuleBehavior("eth_call", ProxyBehavior.Fail);` | Queue cycles fail → forward → forward → fail … |

---

## Request Time Delays

Simulate network latency by adding configurable delays before and/or after each request. Useful for testing timeout handling, loading states, and slow network conditions.

### Configuration

**Via environment variables:**
```bash
PROXY_PRE_DELAY_MS=100 PROXY_POST_DELAY_MS=50 pnpm start
```

**Via API:**
```ts
proxy.setPreDelay(100); // 100ms delay before forwarding request to upstream
proxy.setPostDelay(50); // 50ms delay after receiving response, before sending to client
```

### Delay Types

| Delay | When Applied | Use Case |
|-------|--------------|----------|
| **Pre-delay** | Before forwarding request to upstream | Simulate slow request initiation, test client timeout before response starts |
| **Post-delay** | After receiving upstream response, before sending to client | Simulate slow response delivery, test loading states |

### Example: Testing Slow Network
```ts
const proxy = new ProxyServer(new URL("http://localhost:8545"), 3000, logger);
proxy.setPreDelay(200); // 200ms before each request
proxy.setPostDelay(100); // 100ms after each response
await proxy.start();

// All requests through the proxy will now have 300ms total added latency
```

---

## Testing
To test this package, run:
```bash
pnpm i
pnpm test
pnpm test --run
```

3 changes: 3 additions & 0 deletions scripts/ralph/progress.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ralph Progress Log
Started: Tue 13 Jan 2026 17:11:56 PST
---
56 changes: 54 additions & 2 deletions src/ProxyServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import NodeWS from "ws";
import type { TaggedLogger } from "./utils/logger";
import { waitForCondition } from "./utils/waitForCondition";
import { rawDataToUint8OrString, rawDataToString, truncateForLog, matches } from "./utils/helpers";
import { sleep } from "./utils/sleep";

// Prefer native WebSocket (workers), otherwise node 'ws'
const WebSocketImpl: typeof NodeWS =
Expand Down Expand Up @@ -67,6 +68,10 @@ export class ProxyServer {
// per-method rules (first match wins)
private rules: BehaviorRule[] = [];

// delay configuration
private preDelayMs: number = 0;
private postDelayMs: number = 0;

constructor(
upstreamUrl: string | URL,
proxyPort: number,
Expand All @@ -76,6 +81,16 @@ export class ProxyServer {
this.#UPSTREAM = upstreamUrl instanceof URL ? upstreamUrl : new URL(upstreamUrl);
this.#PROXY_PORT = proxyPort;

// Read delay configuration from environment
const preDelayEnv = process.env.PROXY_PRE_DELAY_MS;
if (preDelayEnv) {
this.preDelayMs = Number.parseInt(preDelayEnv, 10) || 0;
}
const postDelayEnv = process.env.PROXY_POST_DELAY_MS;
if (postDelayEnv) {
this.postDelayMs = Number.parseInt(postDelayEnv, 10) || 0;
}

this.app = new Hono();
this.app.use("*", cors());

Expand Down Expand Up @@ -166,10 +181,27 @@ export class ProxyServer {
this.logger?.trace("All rules cleared");
}

// ------- public API: delay configuration -------
public setPreDelay(ms: number): void {
this.preDelayMs = ms;
this.logger?.trace(`Pre-delay set: ${ms}ms`);
}

public setPostDelay(ms: number): void {
this.postDelayMs = ms;
this.logger?.trace(`Post-delay set: ${ms}ms`);
}

// ------- lifecycle -------
public async start(): Promise<void> {
this.server = serve({ fetch: this.app.fetch, port: this.#PROXY_PORT }) as AnyServer;
this.logger?.info(`Proxy listening on :${this.#PROXY_PORT} → ${this.upstreamHttpUrl}`);
if (this.preDelayMs > 0) {
this.logger?.info(`Pre-delay: ${this.preDelayMs}ms`);
}
if (this.postDelayMs > 0) {
this.logger?.info(`Post-delay: ${this.postDelayMs}ms`);
}
this.injectWebSocket?.(this.server);
}
public async stop(): Promise<void> {
Expand Down Expand Up @@ -268,7 +300,7 @@ export class ProxyServer {
const requestIdMap = new Map<number | string, { method: string; start: number }>();

return {
onMessage: (msg, appClient) => {
onMessage: async (msg, appClient) => {
const parsed = JSON.parse(msg.data.toString());
const paramsStr = parsed.params ? ` ${JSON.stringify(parsed.params)}` : "";
this.logger?.info(`[ws] -> ${parsed.method ?? "unknown"}${paramsStr}`);
Expand All @@ -285,6 +317,11 @@ export class ProxyServer {
setTimeout(() => requestIdMap.delete(parsed.id), 30_000); // GC safeguard
}

// Apply pre-delay before forwarding
if (this.preDelayMs > 0) {
await sleep(this.preDelayMs);
}

waitForCondition(
() => upstream.readyState === WebSocketImpl.OPEN,
1000,
Expand All @@ -306,7 +343,7 @@ export class ProxyServer {
onOpen: (_evt, appClient) => {
this.logger?.info("[ws] client connected");

upstream.addEventListener("message", (raw) => {
upstream.addEventListener("message", async (raw) => {
const text = rawDataToString((raw as any).data ?? raw);
const parsed = JSON.parse(text);

Expand Down Expand Up @@ -334,6 +371,11 @@ export class ProxyServer {
);
}

// Apply post-delay before sending response to client
if (this.postDelayMs > 0) {
await sleep(this.postDelayMs);
}

waitForCondition(
() => appClient.readyState === WebSocketImpl.OPEN,
1000,
Expand Down Expand Up @@ -374,6 +416,11 @@ export class ProxyServer {
const maybe = this.#handleBehaviorHttp(c, behavior);
if (maybe) return maybe;

// Apply pre-delay before forwarding
if (this.preDelayMs > 0) {
await sleep(this.preDelayMs);
}

// Forward
const incoming = new URL(c.req.url);
const targetUrl = new URL(incoming.pathname + incoming.search, this.#UPSTREAM);
Expand All @@ -397,6 +444,11 @@ export class ProxyServer {
`(http) ${ms.toString().padEnd(8, "0")}ms => ${body.method}${paramsStr}${responseLog}`,
);

// Apply post-delay before sending response to client
if (this.postDelayMs > 0) {
await sleep(this.postDelayMs);
}

return c.json(data, resp.status as ContentfulStatusCode);
} catch (error) {
return c.json({ error: `Proxy error: ${JSON.stringify(error)}` }, 500);
Expand Down
2 changes: 2 additions & 0 deletions src/utils/sleep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
66 changes: 66 additions & 0 deletions tasks/prd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"project": "eth-rpc-proxy",
"branchName": "ralph/request-delay",
"description": "Request Delay Feature - Add configurable delay to simulate slow network conditions for testing purposes",
"userStories": [
{
"id": "US-001",
"title": "Add pre-request delay via environment variable",
"description": "As a developer, I want to set a delay before requests are forwarded to upstream so that I can simulate slow network conditions.",
"acceptanceCriteria": [
"PROXY_PRE_DELAY_MS environment variable sets delay in milliseconds before forwarding",
"Delay is applied to both HTTP and WebSocket requests",
"Default value is 0 (no delay)",
"Delay value is logged at startup if set",
"Typecheck passes"
],
"priority": 1,
"passes": true,
"notes": ""
},
{
"id": "US-002",
"title": "Add post-response delay via environment variable",
"description": "As a developer, I want to set a delay after receiving upstream responses so that I can simulate slow response delivery.",
"acceptanceCriteria": [
"PROXY_POST_DELAY_MS environment variable sets delay in milliseconds before sending response to client",
"Delay is applied to both HTTP and WebSocket responses",
"Default value is 0 (no delay)",
"Delay value is logged at startup if set",
"Typecheck passes"
],
"priority": 2,
"passes": true,
"notes": ""
},
{
"id": "US-003",
"title": "Support programmatic delay configuration",
"description": "As a developer using the library, I want to configure delays programmatically so that I can set them in my test setup.",
"acceptanceCriteria": [
"setPreDelay(ms: number) method added to ProxyServer",
"setPostDelay(ms: number) method added to ProxyServer",
"Methods override environment variable values",
"Passing 0 disables the delay",
"Typecheck passes"
],
"priority": 3,
"passes": true,
"notes": ""
},
{
"id": "US-004",
"title": "Add delay tests",
"description": "As a developer, I want tests to verify delay behavior works correctly.",
"acceptanceCriteria": [
"Test verifies pre-delay adds expected latency to request",
"Test verifies post-delay adds expected latency to response",
"Test verifies combined delays work correctly",
"Tests pass with pnpm test"
],
"priority": 4,
"passes": true,
"notes": ""
}
]
}
62 changes: 62 additions & 0 deletions tasks/progress.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## Codebase Patterns
- Use `sleep` utility from `src/utils/sleep.ts` for async delays
- ProxyServer reads env vars in constructor for configuration
- Pre-delay is applied in both HTTP (#setupHttpProxy) and WebSocket (#setupWebsocketProxy onMessage) handlers
- Logging via `this.logger?.info()` and `this.logger?.trace()`

---

# Progress Log

## 2026-01-13 - US-001
Thread: https://ampcode.com/threads/T-019bba0f-34e1-716c-8dcc-243ea8aa9eeb
- Implemented pre-request delay via PROXY_PRE_DELAY_MS environment variable
- Created `src/utils/sleep.ts` utility function
- Added `preDelayMs` property to ProxyServer read from env
- Applied pre-delay before forwarding in HTTP handler (before fetch)
- Applied pre-delay in WebSocket handler (before upstream.send)
- Added logging at startup when pre-delay is set
- Files changed: src/ProxyServer.ts, src/utils/sleep.ts
- **Learnings for future iterations:**
- Use node v18+ for TypeScript compilation (tsc path: node_modules/typescript/bin/tsc)
- WebSocket onMessage handler made async to support await for delay
---


## 2026-01-13 - US-002
Thread: https://ampcode.com/threads/T-019bba13-c92b-742d-b948-cea11f21922b
- Implemented post-response delay via PROXY_POST_DELAY_MS environment variable
- Added `postDelayMs` property to ProxyServer read from env
- Applied post-delay in HTTP handler (after fetch response, before returning to client)
- Applied post-delay in WebSocket handler (made upstream message listener async, added await before sending to client)
- Added logging at startup when post-delay is set
- Files changed: src/ProxyServer.ts
- **Learnings for future iterations:**
- WebSocket upstream message handler needed to be made async to support await for delay
- Pattern mirrors pre-delay but applied after receiving upstream response
---

## 2026-01-13 - US-003
Thread: https://ampcode.com/threads/T-019bba15-57b0-745e-8874-150377be8941
- Added setPreDelay(ms: number) and setPostDelay(ms: number) methods to ProxyServer
- Methods directly set preDelayMs and postDelayMs properties, overriding env var values
- Added trace logging when methods are called
- Files changed: src/ProxyServer.ts
- **Learnings for future iterations:**
- Existing delay properties (preDelayMs, postDelayMs) are already private, so methods just set them directly
- bun is available for running tsc when pnpm/npx/node aren't in PATH
---

## 2026-01-13 - US-004
Thread: https://ampcode.com/threads/T-019bba16-9816-76d9-a60b-ddd2509fb8c5
- Added delay tests in tests/delay.test.ts
- Tests verify pre-delay adds latency to HTTP and WebSocket requests
- Tests verify post-delay adds latency to HTTP and WebSocket responses
- Tests verify combined pre+post delays work correctly
- Tests verify setting delay to 0 disables it
- Files changed: tests/delay.test.ts
- **Learnings for future iterations:**
- Use different ports (4011/4012) for test isolation to avoid conflicts with other test files
- beforeEach resets delays to 0 for test isolation
- Use performance.now() for timing measurements with tolerance margin (~10-15ms)
---
Loading