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
- 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
- Check the child's stderr in logs (logWriter forwards deltachat-rpc-server stderr to debug logs) to find why it died
- Verify the server binary path and that DC_ACCOUNTS_PATH points to a writable, non-corrupted data directory
- 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
- Stop inbound processing and agent reply paths before closing the rpcClient
- Keep one owner of the rpcClient lifecycle; never share it across restarts
- Monitor the deltachat-rpc-server child process exit (readLoop EOF) and alert
- Never retry call() on the same client after 'connection closed' — rebuild instead
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
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- deltachat rpc write %s: %w
- deltachat create chatmail account on %s: %w
- deltachat created account on %s, but could not read generate
- deltachat created account on %s, but generated email is empt
- get config %s: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/2f06bcf49d186bf5.
Report an issue: GitHub.