{"record":{"id":"2f06bcf49d186bf5","repo":"sipeed/picoclaw","slug":"deltachat-rpc-connection-closed","errorCode":null,"errorMessage":"deltachat rpc: connection closed","messagePattern":"deltachat rpc: connection closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/channels/deltachat/rpc.go","lineNumber":126,"sourceCode":"\n// failAll wakes every pending caller with an error (used on EOF / shutdown).\nfunc (c *rpcClient) failAll(err error) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tc.closed = true\n\tfor id, ch := range c.pending {\n\t\tch <- rpcResponse{Error: &rpcError{Code: -1, Message: \"rpc closed: \" + err.Error()}}\n\t\tdelete(c.pending, id)\n\t}\n}\n\n// call issues one request and blocks until the matching response arrives, the\n// context is canceled, or the server goes away.\nfunc (c *rpcClient) call(ctx context.Context, method string, params ...any) (json.RawMessage, error) {\n\tc.mu.Lock()\n\tif c.closed {\n\t\tc.mu.Unlock()\n\t\treturn nil, fmt.Errorf(\"deltachat rpc: connection closed\")\n\t}\n\tc.nextID++\n\tid := c.nextID\n\tch := make(chan rpcResponse, 1)\n\tc.pending[id] = ch\n\tc.mu.Unlock()\n\n\treq := rpcRequest{JSONRPC: \"2.0\", ID: id, Method: method, Params: params}\n\tdata, err := json.Marshal(req)\n\tif err != nil {\n\t\tc.clearPending(id)\n\t\treturn nil, err\n\t}\n\tdata = append(data, '\\n')\n\n\tc.mu.Lock()\n\t_, err = c.stdin.Write(data)\n\tc.mu.Unlock()","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/deltachat/rpc.go#L108-L144","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","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)"],"exampleFix":"// before\nres, err := c.rpc.call(ctx, \"get_chatlist\", 0, 0, 60)\nif err != nil {\n    log.Printf(\"rpc failed: %v\", err) // keeps failing: connection is dead\n}\n\n// after\nif _, err := c.rpc.call(ctx, method, params...); err != nil {\n    if strings.Contains(err.Error(), \"connection closed\") {\n        _ = c.rpc.close()\n        if c.rpc, err = startRPC(serverPath, dataDir); err != nil {\n            return err // respawn failed; surface it\n        }\n        _, err = c.rpc.call(ctx, method, params...) // retry once on fresh client\n    }\n}","handlingStrategy":"retry","validationCode":"// Pre-flight: probe the RPC connection before critical calls\nif _, err := rpc.call(ctx, \"get_system_info\"); err != nil {\n    rpc, err = startRPC(serverPath, dataDir) // respawn the child process\n    if err != nil {\n        return fmt.Errorf(\"deltachat rpc unavailable: %w\", err)\n    }\n}","typeGuard":"func rpcConnClosed(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"connection closed\")\n}","tryCatchPattern":"if _, err := rpc.call(ctx, method, params...); err != nil {\n    if rpcConnClosed(err) {\n        _ = rpc.close()\n        if rpc, err = startRPC(serverPath, dataDir); err != nil {\n            return err\n        }\n        _, err = rpc.call(ctx, method, params...) // single retry on fresh client\n    }\n    if err != nil {\n        return err\n    }\n}","preventionTips":["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"],"tags":["deltachat","rpc","process","connection","shutdown"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}