sipeed/picoclaw · error

deltachat rpc: connection closed

Error message

deltachat rpc: connection closed

What it means

Returned by rpcClient.call when the JSON-RPC stdio connection to the deltachat-rpc-server child process is already marked closed. The client sets closed=true in failAll() (after the stdout read loop hits EOF or a read error) and in close(), so every subsequent call fails fast at the top of call() instead of blocking forever or writing to a dead pipe. It means the child process is gone and the whole rpcClient instance is unusable.

Source

Thrown at pkg/channels/deltachat/rpc.go:126

// failAll wakes every pending caller with an error (used on EOF / shutdown).
func (c *rpcClient) failAll(err error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.closed = true
	for id, ch := range c.pending {
		ch <- rpcResponse{Error: &rpcError{Code: -1, Message: "rpc closed: " + err.Error()}}
		delete(c.pending, id)
	}
}

// call issues one request and blocks until the matching response arrives, the
// context is canceled, or the server goes away.
func (c *rpcClient) call(ctx context.Context, method string, params ...any) (json.RawMessage, error) {
	c.mu.Lock()
	if c.closed {
		c.mu.Unlock()
		return nil, fmt.Errorf("deltachat rpc: connection closed")
	}
	c.nextID++
	id := c.nextID
	ch := make(chan rpcResponse, 1)
	c.pending[id] = ch
	c.mu.Unlock()

	req := rpcRequest{JSONRPC: "2.0", ID: id, Method: method, Params: params}
	data, err := json.Marshal(req)
	if err != nil {
		c.clearPending(id)
		return nil, err
	}
	data = append(data, '\n')

	c.mu.Lock()
	_, err = c.stdin.Write(data)
	c.mu.Unlock()

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Recreate the rpcClient: stop and restart the Delta Chat channel (or call startRPC again) to spawn a fresh deltachat-rpc-server process; retrying on the same client always fails
  2. Check the child's stderr in logs (logWriter forwards deltachat-rpc-server stderr to debug logs) to find why it died
  3. Verify the server binary path and that DC_ACCOUNTS_PATH points to a writable, non-corrupted data directory
  4. Audit lifecycle: ensure no goroutine issues RPC calls after Stop (single ownership of the client)

Example fix

// before
res, err := c.rpc.call(ctx, "get_chatlist", 0, 0, 60)
if err != nil {
    log.Printf("rpc failed: %v", err) // keeps failing: connection is dead
}

// after
if _, err := c.rpc.call(ctx, method, params...); err != nil {
    if strings.Contains(err.Error(), "connection closed") {
        _ = c.rpc.close()
        if c.rpc, err = startRPC(serverPath, dataDir); err != nil {
            return err // respawn failed; surface it
        }
        _, err = c.rpc.call(ctx, method, params...) // retry once on fresh client
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: probe the RPC connection before critical calls
if _, err := rpc.call(ctx, "get_system_info"); err != nil {
    rpc, err = startRPC(serverPath, dataDir) // respawn the child process
    if err != nil {
        return fmt.Errorf("deltachat rpc unavailable: %w", err)
    }
}

Type guard

func rpcConnClosed(err error) bool {
    return err != nil && strings.Contains(err.Error(), "connection closed")
}

Try / catch

if _, err := rpc.call(ctx, method, params...); err != nil {
    if rpcConnClosed(err) {
        _ = rpc.close()
        if rpc, err = startRPC(serverPath, dataDir); err != nil {
            return err
        }
        _, err = rpc.call(ctx, method, params...) // single retry on fresh client
    }
    if err != nil {
        return err
    }
}

Prevention

When it happens

Trigger: Any rpc method call (e.g. get_chatlist, send_text_message) issued after: the deltachat-rpc-server process exited or crashed (readLoop got EOF -> failAll set closed), rpcClient.close() ran (channel Stop/shutdown), or a call races with an in-flight shutdown. The check happens under c.mu at the very start of call().

Common situations: deltachat-rpc-server crashing on a corrupted DC_ACCOUNTS_PATH database, OOM killer reaping the child in a container, channel stopped while an async reply/agent path still issues an RPC, version-mismatched rpc-server binary exiting immediately, calling channel APIs after Stop.

Understand the failure class

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/2f06bcf49d186bf5. Report an issue: GitHub.