Skip to content

server: provider requests hang after host IP change until restart #53442

Open

Description

@yudivian

Summary

While the app server is running (opencode serve --service), changing the local IP of the host's active network interface (e.g., switching NetworkManager connection profiles on the same NIC) leaves the provider connections bound to the previous source IP. The sockets stay in ESTAB but are effectively blackholed, no new connections are opened, provider requests hang indefinitely and no error is logged. Restarting the app fixes it immediately.

Environment

  • opencode version: v2.0.16
  • OS: Ubuntu 24.10, Linux 6.14.6-061406-generic (x86_64)
  • Terminal: xterm-256color (truecolor), no TERM_PROGRAM reported
  • Shell: /bin/bash
  • Install/channel: official binary (~/.opencode/bin/opencode), latest
  • Active plugins: none

Reproduction

  1. Start opencode while the machine has OLD_IP on the active interface (e.g., a DHCP profile). The server establishes long-lived connections to the provider (e.g., opencode.ai:443, models.opencode.ai:443).
  2. Without stopping opencode, switch the same interface to a different profile that assigns NEW_IP (e.g., nmcli con up <static-profile>), or otherwise change the interface address.
  3. Inspect the server sockets with ss -tnp | grep opencode. They still show OLD_IP:<port> -> provider:443 (ESTAB), while ip addr shows NEW_IP on the interface. No new connections are opened, even after 15+ seconds.
  4. Send a chat message / trigger any provider request. It hangs. The log shows no WARN/ERROR entries during the hang and no user-visible error is surfaced.
  5. Switch back to the original profile (OLD_IP returns): pending requests resume without restarting the app.

Immediate workaround: fully restart the app (or the service process). After that, ss shows the connections re-established from NEW_IP and provider requests succeed.

Expected Behavior

  • The app should detect network/interface address changes and re-establish provider connections using the current source address.
  • At minimum it should fail fast (short connect/read timeouts or TCP keepalive) and surface an error instead of hanging indefinitely.

Actual Behavior

  • Existing provider sockets remain bound to the previous IP and continue to be used after the host address changes. Requests hang silently with no log output and no reconnect. Only restarting the app (or restoring the old IP) recovers it.

Additional Context

  • Network connectivity itself is fine during the hang: from the same host and interface, curl to the provider endpoints succeeds with a valid TLS chain and HTTP 200 (opencode.ai, models.opencode.ai, /zen/v1/...), and DNS resolution is correct. Only the already-running app instance is affected.

  • Consistently reproducible across repeated profile switches on the same NIC.

  • Appears to affect both the background service and the client: after a restart, new connections from the current IP appear for both processes.

  • Sanitized ss output captured while the hang was active (interface already had NEW_IP):

    ESTAB 0 0 OLD_IP:PORT  provider:443  users:(("opencode",pid=...,fd=24))
    ESTAB 0 0 OLD_IP:PORT  provider:443  users:(("opencode",pid=...,fd=25))
    
  • No errors in the app log during the hang; periodic provider.updated / model.updated events only appear once connectivity is valid again.

  • Suggestion: subscribe to netlink address-change events (or NetworkManager D-Bus signals) and reset the provider connection pool; additionally add connection timeouts/keepalives so a stale pooled socket fails fast and can be retried with the current source address.

Activity

  1. github-actions commented on Oct 5, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of an existing issue. Please check:

    • #52564: Reports long-lived connections in opencode serve failing to recover after a network change until the server process is restarted. That report is MCP-specific, but the network-change and stale-connection behavior is similar.
  2. opencodejr commented on Oct 27, 2026

    @opencodejr

    Thanks for the detailed report. I can reproduce this on v2.0.16 and on the latest dev (f7d0ce1), using a local mock provider instead of opencode.ai.

    What I did:

    • Put a mock OpenAI-compatible provider in a separate network namespace, reached through a veth pair.
    • Started opencode serve and sent one prompt. It works, and ss shows a pooled keep-alive socket from the original address.
    • Swapped the host-side address (ip addr del the old one, ip addr add a new one). curl to the provider from the new address still works.
    • Sent another prompt in the same session. It hangs.

    What I saw:

    • The request is written to the old socket, still bound to the old address, and piles up in its send queue.
    • No new connection is opened, the provider never receives the request, and the server logs no warning or error.
    • On a new session, the title request and the reply go out together. One gets the stale socket and hangs; the other opens a fresh connection and succeeds.

    v2.0.16 has no transport timeout, so the hang there should last indefinitely. Current dev has a default 5-minute response-header timeout, so the request probably fails and retries after about 5 minutes, but I didn't confirm that. Either way the stale socket isn't detected and the prompt hangs silently for minutes.

    The script below reproduces it (it needs root for the network namespace). Related issues: #53747 (steps stalling with no timeout) and #52866 (stall timeout and keepalive handling).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduce #53442: provider requests hang after the host's source IP changes.
    # Needs root (network namespaces), bun, iproute2, curl, and `opencode` on PATH
    # (or OPENCODE_CMD="bun ... packages/cli/src/index.ts" for a source checkout).
    # A mock OpenAI-compatible provider runs in a separate netns behind a veth pair;
    # the host side address is swapped from .2 to .3 between two prompts.
    set -u
    NET=${NET:-10.9.1}; NS=${NS:-prov53442}; VH=${VH:-vh53442}; PORT=${PORT:-4299}
    OPENCODE_CMD=${OPENCODE_CMD:-opencode}
    WORK=$(mktemp -d /tmp/opencode/r53442.XXXX); mkdir -p "$WORK/proj/.opencode" "$WORK/home"
    export HOME="$WORK/home" OPENCODE_SERVER_PASSWORD=pw
    
    ip netns add "$NS"; ip link add "$VH" type veth peer name "${VH}p"
    ip link set "${VH}p" netns "$NS"
    ip addr add "$NET.2/24" dev "$VH"; ip link set "$VH" up
    ip -n "$NS" addr add "$NET.1/24" dev "${VH}p"; ip -n "$NS" link set "${VH}p" up
    
    cat > "$WORK/mock.ts" <<'EOF'
    Bun.serve({ hostname: "0.0.0.0", port: 8080, idleTimeout: 0, async fetch(req) {
      const u = new URL(req.url)
      if (!u.pathname.endsWith("/chat/completions")) return Response.json({ data: [] })
      const body: any = await req.json(); console.log(new Date().toISOString(), "POST", JSON.stringify(body.messages?.at(-1)?.content).slice(0, 60))
      const enc = new TextEncoder(), b = { id: "c1", object: "chat.completion.chunk", created: 1, model: "m" }
      const ev = (o: any) => enc.encode(`data: ${JSON.stringify(o)}\n\n`)
      return new Response(new ReadableStream({ start(c) {
        c.enqueue(ev({ ...b, choices: [{ index: 0, delta: { role: "assistant", content: "pong" }, finish_reason: null }] }))
        c.enqueue(ev({ ...b, choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }))
        c.enqueue(enc.encode("data: [DONE]\n\n")); c.close()
      } }), { headers: { "content-type": "text/event-stream" } })
    } })
    EOF
    cat > "$WORK/proj/.opencode/opencode.jsonc" <<EOF
    { "model": "mock/m", "snapshots": false,
      "permissions": [{ "action": "*", "resource": "*", "effect": "allow" }],
      "providers": { "mock": { "name": "Mock", "package": "@opencode-ai/ai/providers/openai/chat",
        "settings": { "apiKey": "k", "baseURL": "http://$NET.1:8080/v1" },
        "models": { "m": { "name": "M", "capabilities": { "tools": false, "input": ["text"], "output": ["text"] },
          "limit": { "context": 128000, "output": 1000 } } } } } }
    EOF
    ip netns exec "$NS" bun "$WORK/mock.ts" > "$WORK/mock.log" 2>&1 &
    cd "$WORK/proj" && git init -q
    $OPENCODE_CMD serve --hostname 127.0.0.1 --port "$PORT" --print-logs --log-level info > "$WORK/serve.log" 2>&1 &
    sleep 8
    run() { local s=$(date +%s); timeout "${WAIT:-90}" $OPENCODE_CMD run --server "http://127.0.0.1:$PORT" -m mock/m "$@" > "$WORK/run.log" 2>&1; echo "  exit=$? after $(( $(date +%s)-s ))s, output: $(tail -1 "$WORK/run.log")"; }
    
    echo "== prompt 1 (source $NET.2)"; run "hi"
    ss -tn | grep "$NET.1:8080"
    echo "== host address $NET.2 -> $NET.3"
    ip addr del "$NET.2/24" dev "$VH"; ip addr add "$NET.3/24" dev "$VH"
    echo "== curl from the new address works:"; curl -s -m3 "http://$NET.1:8080/v1/models"; echo
    echo "== prompt 2 (same session)"; run -c "after ip change"
    echo "== sockets:"; ss -tno | grep "$NET.1:8080"
    echo "== requests the mock provider received:"; cat "$WORK/mock.log"
    echo "== WARN/ERROR in server log:"; grep -E "level=(WARN|ERROR)" "$WORK/serve.log" || echo "  (none)"
    kill %1 %2 2>/dev/null; ip netns del "$NS"; ip link del "$VH" 2>/dev/null

    Run it: bash repro.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions