{"record":{"id":"ca54bb20eb9d01f9","repo":"wavetermdev/waveterm","slug":"timeout-sending-request","errorCode":null,"errorMessage":"timeout sending request","messagePattern":"timeout sending request","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/wshutil/wshrpc.go","lineNumber":807,"sourceCode":"\t}\n\treq := &RpcMessage{\n\t\tCommand: command,\n\t\tReqId:   handler.reqId,\n\t\tData:    data,\n\t\tTimeout: timeoutMs,\n\t\tRoute:   opts.Route,\n\t}\n\tbarr, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thandler.respCh = w.registerRpc(handler, command, opts.Route, handler.reqId)\n\tselect {\n\tcase w.OutputCh <- barr:\n\t\treturn handler, nil\n\tcase <-handler.ctx.Done():\n\t\thandler.finalize()\n\t\treturn nil, fmt.Errorf(\"timeout sending request\")\n\t}\n}\n\nfunc (w *WshRpc) IsServerDone() bool {\n\tw.Lock.Lock()\n\tdefer w.Lock.Unlock()\n\treturn w.ServerDone\n}\n\nfunc (w *WshRpc) setServerDone() {\n\tw.Lock.Lock()\n\tdefer w.Lock.Unlock()\n\tw.ServerDone = true\n\tclose(w.CtxDoneCh)\n\tutilfn.DrainChannelSafe(w.InputCh, \"wshrpc.setServerDone\")\n}\n\nfunc (w *WshRpc) retrySendTimeout(resId string) {","sourceCodeStart":789,"sourceCodeEnd":825,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/wshutil/wshrpc.go#L789-L825","documentation":"SendComplexRequest registers the request handler and then writes the request bytes to OutputCh; if the handler's context is done before the write is accepted, registration is finalized and this error is returned — the request never made it onto the wire.","triggerScenarios":"Calling a request with a context that is already cancelled/expired; connection shutdown closing OutputCh/consumer while the request is being sent; per-request deadline shorter than the time needed to enqueue the packet.","commonSituations":"Firing RPCs during teardown of a block/connection; callers passing an already-cancelled context; timeouts configured too aggressively (or DefaultTimeoutMs too small for a congested output channel).","solutions":["Check ctx.Err() before sending and avoid issuing requests with cancelled/expired contexts.","Increase the timeout via RpcOpts (TimeoutMs) if the output channel can be slow.","Ensure the connection read loop keeps running and draining OutputCh during the request.","Retry the request on a fresh, live connection/context after verifying the peer is up."],"exampleFix":"// before\nctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)\nresp, err := w.SendComplexRequest(\"waveclient:get\", args, &wshutil.RpcOpts{Route: route})\n// after\nctx, cancel := context.WithTimeout(ctx, 5*time.Second)\nresp, err := w.SendComplexRequest(\"waveclient:get\", args, &wshutil.RpcOpts{Route: route, TimeoutMs: 5000})","handlingStrategy":"try-catch","validationCode":"if ctx.Err() != nil {\n    return fmt.Errorf(\"cannot send request: context already done: %w\", ctx.Err())\n}","typeGuard":null,"tryCatchPattern":"handler, err := w.SendComplexRequest(cmd, args, opts)\nif err != nil && strings.Contains(err.Error(), \"timeout sending request\") {\n    // output channel blocked or ctx expired; check connection health, then retry with a longer timeout\n    return retryWithBackoff(func() error { _, err := w.SendComplexRequest(cmd, args, opts); return err })\n}","preventionTips":["Never pass already-cancelled contexts into request calls.","Use realistic TimeoutMs values for your workload.","Ensure the connection read loop stays alive to drain OutputCh.","Retry idempotent requests with backoff after connection recovery."],"tags":["rpc","timeout","request-send"],"backgroundTag":"rpc-timeout-waiting-for-response","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}