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

  1. Restart the channel/rpcClient to respawn the child process; the pipe cannot be repaired
  2. 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
  3. Verify deltachat-rpc-server version compatibility and that DC_ACCOUNTS_PATH data dir is writable
  4. 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

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


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