sipeed/picoclaw · error
deltachat rpc write %s: %w
Error message
deltachat rpc write %s: %w
What it means
Writing the newline-delimited JSON-RPC request to the deltachat-rpc-server's stdin pipe failed; %s is the RPC method name and the wrapped error is the pipe error (typically os.ErrClosed or EPIPE/broken pipe). It means the child died between the closed-flag check and the write, or the pipe was closed by close(). The pending request is cleaned up via clearPending before returning.
Source
Thrown at pkg/channels/deltachat/rpc.go:147
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()
if err != nil {
c.clearPending(id)
return nil, fmt.Errorf("deltachat rpc write %s: %w", method, err)
}
select {
case <-ctx.Done():
c.clearPending(id)
return nil, ctx.Err()
case resp := <-ch:
if resp.Error != nil {
return nil, resp.Error
}
return resp.Result, nil
}
}
func (c *rpcClient) clearPending(id uint64) {
c.mu.Lock()
delete(c.pending, id)
c.mu.Unlock()View on GitHub (pinned to 49183d7e8d)
Solutions
- Restart the channel/rpcClient to respawn the child process; the pipe cannot be repaired
- Check the method name in the message to see which call hit the dead pipe, and the child's stderr logs for the crash cause
- Verify deltachat-rpc-server version compatibility and that DC_ACCOUNTS_PATH data dir is writable
- Ensure no Send/RPC path runs after Stop; serialize lifecycle transitions with the call mutex
Example fix
// before
if _, err := c.stdin.Write(data); err != nil {
return nil, err // no method context, callers cannot tell what failed
}
// after (as implemented, plus caller-side respawn)
if _, err := c.stdin.Write(data); err != nil {
c.clearPending(id)
return nil, fmt.Errorf("deltachat rpc write %s: %w", method, err)
}
// caller: on errors.Is(err, os.ErrClosed) || errors.Is(err, syscall.EPIPE), rebuild via startRPC() Defensive patterns
Strategy: retry
Type guard
func isBrokenPipe(err error) bool {
return errors.Is(err, os.ErrClosed) || errors.Is(err, syscall.EPIPE)
} Try / catch
if _, err := rpc.call(ctx, "send_text_message", chatID, text); err != nil {
if isBrokenPipe(err) { // method name is in the message, cause in the chain
rpc, err = startRPC(serverPath, dataDir)
if err != nil {
return err
}
_, err = rpc.call(ctx, "send_text_message", chatID, text)
}
if err != nil {
return err
}
} Prevention
- Serialize close() and call() behind the same mutex ownership so writes never race shutdown
- Watch the child process and respawn proactively on exit instead of discovering via EPIPE
- Keep deltachat-rpc-server version pinned and compatible with the expected RPC schema
- Log the method name from the error message to identify which call hit the dead pipe
When it happens
Trigger: c.stdin.Write(data) inside call() fails when: the child process exited after the c.closed check passed (race window), rpcClient.close() closed the stdin pipe concurrently, or the server crashed mid-session leaving a broken pipe.
Common situations: deltachat-rpc-server crash from a bad accounts DB, calling RPC methods concurrently with Stop/close, child killed by OOM or container resource limits, binary version mismatch causing immediate exit.
Related errors
- deltachat rpc: connection closed
- 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/c1e2dca04c6df8e3.
Report an issue: GitHub.