{"record":{"id":"5dc0d238aa62c2cc","repo":"wavetermdev/waveterm","slug":"request-already-done-cannot-send-additional-respo","errorCode":null,"errorMessage":"request already done, cannot send additional response","messagePattern":"request already done, cannot send additional response","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/wshutil/wshrpc.go","lineNumber":686,"sourceCode":"\t\tCommand: wshrpc.Command_Message,\n\t\tData: wshrpc.CommandMessageData{\n\t\t\tMessage: msg,\n\t\t},\n\t\tRoute: handler.source, // send back to source\n\t}\n\tmsgBytes, _ := json.Marshal(rpcMsg) // will never fail\n\tselect {\n\tcase handler.w.OutputCh <- msgBytes:\n\tcase <-handler.ctx.Done():\n\t}\n}\n\nfunc (handler *RpcResponseHandler) SendResponse(data any, done bool) error {\n\tdefer func() {\n\t\tpanichandler.PanicHandler(\"SendResponse\", recover())\n\t}()\n\tif handler.done.Load() {\n\t\treturn fmt.Errorf(\"request already done, cannot send additional response\")\n\t}\n\tif done {\n\t\tdefer handler.close()\n\t}\n\tif handler.reqId == \"\" {\n\t\treturn nil\n\t}\n\tmsg := &RpcMessage{\n\t\tResId: handler.reqId,\n\t\tData:  data,\n\t\tCont:  !done,\n\t}\n\tbarr, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tselect {\n\tcase handler.w.OutputCh <- barr:","sourceCodeStart":668,"sourceCodeEnd":704,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/wshutil/wshrpc.go#L668-L704","documentation":"RpcResponseHandler.SendResponse guards with an atomic done flag: once a request has been finalized (a done response sent, cancelled, or timed out), further SendResponse calls are rejected with this error to prevent double responses or writes to unregistered requests.","triggerScenarios":"Calling SendResponse twice on the same handler; calling SendResponse after SendResponseError already sent a done response; responding after the request was cancelled/timed out; streaming code continuing to push data after sending the final done=true packet.","commonSituations":"Streaming RPC handlers with multiple return paths that each send a final response; error paths that both log-and-respond and fall through to a second respond; cancel/timeout racing with normal completion.","solutions":["Send exactly one done response: guard with handler.ResponseDone() or a local flag before the final SendResponse.","Use SendResponseError as the single exit point for failure paths instead of responding in multiple places.","Check handler.done / ResponseDone() before responding after cancellation or timeout.","Restructure streaming handlers so the final done packet is sent in one defer/one place."],"exampleFix":"// before\nhandler.SendResponse(chunk1, false)\nhandler.SendResponse(chunk2, true)\nif err != nil { handler.SendResponseError(err) } // second done response\n// after\nhandler.SendResponse(chunk1, false)\nif err != nil { handler.SendResponseError(err); return }\nhandler.SendResponse(chunk2, true)","handlingStrategy":"try-catch","validationCode":"if handler.ResponseDone() {\n    return fmt.Errorf(\"skip: response already sent for req %s\", handler.reqId)\n}","typeGuard":null,"tryCatchPattern":"if err := handler.SendResponse(data, done); err != nil && strings.Contains(err.Error(), \"already done\") {\n    // a done response/cancel/timeout won the race; log and stop sending further responses\n    return nil\n}","preventionTips":["Send exactly one done=true response per request handler.","Centralize the final response in a single code path (or defer).","Check ResponseDone()/done before responding after cancellation or timeout.","In streaming handlers, send data with done=false and reserve done=true for the final chunk only."],"tags":["rpc","double-response","lifecycle"],"backgroundTag":"rpc-response-already-sent","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}