@@ -3,6 +3,7 @@ package api
33import (
44 "context"
55 "encoding/json"
6+ "errors"
67 "fmt"
78 "io"
89 "net/http"
@@ -15,6 +16,15 @@ import (
1516 mw "github.com/onkernel/hypeman/lib/middleware"
1617)
1718
19+ // cpErrorSent wraps an error that has already been sent to the client.
20+ // The caller should log this error but not send it again to avoid duplicates.
21+ type cpErrorSent struct {
22+ err error
23+ }
24+
25+ func (e * cpErrorSent ) Error () string { return e .err .Error () }
26+ func (e * cpErrorSent ) Unwrap () error { return e .err }
27+
1828// CpRequest represents the JSON body for copy requests
1929type CpRequest struct {
2030 // Direction: "to" copies from client to guest, "from" copies from guest to client
@@ -155,8 +165,12 @@ func (s *ApiService) CpHandler(w http.ResponseWriter, r *http.Request) {
155165 "subject" , subject ,
156166 "duration_ms" , duration .Milliseconds (),
157167 )
158- errMsg , _ := json .Marshal (CpError {Type : "error" , Message : cpErr .Error ()})
159- ws .WriteMessage (websocket .TextMessage , errMsg )
168+ // Only send error message if it hasn't already been sent to the client
169+ var sentErr * cpErrorSent
170+ if ! errors .As (cpErr , & sentErr ) {
171+ errMsg , _ := json .Marshal (CpError {Type : "error" , Message : cpErr .Error ()})
172+ ws .WriteMessage (websocket .TextMessage , errMsg )
173+ }
160174 return
161175 }
162176
@@ -205,6 +219,7 @@ func (s *ApiService) handleCopyTo(ctx context.Context, ws *websocket.Conn, inst
205219 }
206220
207221 // Read data chunks from WebSocket and forward to guest
222+ var receivedEndMessage bool
208223 for {
209224 msgType , data , err := ws .ReadMessage ()
210225 if err != nil {
@@ -219,6 +234,7 @@ func (s *ApiService) handleCopyTo(ctx context.Context, ws *websocket.Conn, inst
219234 var msg map [string ]interface {}
220235 if json .Unmarshal (data , & msg ) == nil {
221236 if msg ["type" ] == "end" {
237+ receivedEndMessage = true
222238 break
223239 }
224240 }
@@ -232,6 +248,11 @@ func (s *ApiService) handleCopyTo(ctx context.Context, ws *websocket.Conn, inst
232248 }
233249 }
234250
251+ // If the WebSocket closed without receiving an end message, the transfer is incomplete
252+ if ! receivedEndMessage {
253+ return fmt .Errorf ("client disconnected before completing transfer" )
254+ }
255+
235256 // Send end message to guest
236257 if err := stream .Send (& guest.CopyToGuestRequest {
237258 Request : & guest.CopyToGuestRequest_End {End : & guest.CopyToGuestEnd {}},
@@ -255,8 +276,10 @@ func (s *ApiService) handleCopyTo(ctx context.Context, ws *websocket.Conn, inst
255276 resultJSON , _ := json .Marshal (result )
256277 ws .WriteMessage (websocket .TextMessage , resultJSON )
257278
258- // Don't return an error here - the result message already contains any error info.
259- // Returning an error would cause the caller to send a duplicate error message.
279+ if ! resp .Success {
280+ // Return a wrapped error so the caller logs it correctly but doesn't send a duplicate
281+ return & cpErrorSent {err : fmt .Errorf ("copy to guest failed: %s" , resp .Error )}
282+ }
260283 return nil
261284}
262285
@@ -334,9 +357,8 @@ func (s *ApiService) handleCopyFrom(ctx context.Context, ws *websocket.Conn, ins
334357 }
335358 errJSON , _ := json .Marshal (cpErr )
336359 ws .WriteMessage (websocket .TextMessage , errJSON )
337- // Don't return an error here - the error message was already sent to the client.
338- // Returning an error would cause the caller to send a duplicate error message.
339- return nil
360+ // Return a wrapped error so the caller logs it correctly but doesn't send a duplicate
361+ return & cpErrorSent {err : fmt .Errorf ("copy from guest failed: %s" , r .Error .Message )}
340362 }
341363 }
342364
0 commit comments