Skip to content

[Bug]: GatewayClient.request() has no timeout, causing indefinite hangs #4954

@coygeek

Description

@coygeek

Summary

Severity: P1/High (Score: 85/150)
CWE: CWE-400 - Uncontrolled Resource Consumption
OWASP: A05:2021 - Security Misconfiguration
File: src/gateway/client.ts:382-407

The GatewayClient.request() method returns a Promise that has no timeout mechanism. If the gateway server never responds (network partition, crash, packet loss), the promise hangs forever, leaking memory and blocking callers indefinitely.

Why this is critical: Gateway client requests are used throughout the codebase for inter-service communication. Network partitions, server crashes, and packet loss are inevitable in distributed systems. Without timeouts, a single unresponsive gateway causes cascading hangs—callers block waiting for responses that never come, the pending Map accumulates entries that leak memory, and upstream timeouts (if any) don't clean up downstream state. The codebase already has a good example of timeout handling in IMessageRpcClient—this should follow the same pattern.

Triage Assessment

Factor Value Score
Reachability Network conditions trigger (not attacker-controlled) 20/40
Impact Memory leak, workflow stalls 25/50
Exploitability Passive (network instability) 15/30
Verification file:line ✓, code ✓, positive example cited 25/30
Total 85/150

Steps to reproduce

  1. Establish a gateway client connection
  2. Make a request via client.request('method', params)
  3. Kill the gateway server without closing the connection cleanly
  4. Observe the request promise never resolves or rejects
  5. Memory in this.pending Map grows with each stuck request

Expected behavior

Requests should timeout after a configurable duration (e.g., 30 seconds), rejecting with a timeout error and cleaning up the pending entry.

Actual behavior

Promises stored in this.pending map have no timeout mechanism:

Affected code location:

Gateway Client (src/gateway/client.ts:382-407):

async request<T = unknown>(
  method: string,
  params?: unknown,
  opts?: { expectFinal?: boolean },
): Promise<T> {
  // ...
  const p = new Promise<T>((resolve, reject) => {
    this.pending.set(id, {
      resolve: (value) => resolve(value as T),
      reject,
      expectFinal,
    });
  });
  this.ws.send(JSON.stringify(frame));
  return p;  // <-- No timeout!
}

Environment

  • Version: latest (main branch)
  • OS: Any
  • Install method: Any

Positive example in codebase

The IMessageRpcClient.request() (src/imessage/client.ts:127-163) has proper timeout handling:

const timeout = setTimeout(() => {
  this.pending.delete(id);
  reject(new Error(`RPC timeout for ${method}`));
}, this.timeoutMs);

Impact

  • Memory leak: Each hung request leaks a pending Map entry
  • Workflow stalls: Callers awaiting these promises block indefinitely
  • Resource exhaustion: Under partial network failures, many requests can accumulate
  • No visibility: No error or log indicates the hang

Recommended fix

Add timeout to GatewayClient.request():

const timeout = setTimeout(() => {
  this.pending.delete(id);
  reject(new Error(`Gateway request timeout for ${method}`));
}, this.timeoutMs ?? 30000);

// In the resolve/reject handlers:
clearTimeout(timeout);

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstaleMarked as stale due to inactivity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions