From 96dd604789d21a7f5a44f0b13c4a7214975c82a4 Mon Sep 17 00:00:00 2001 From: Jeremie Fraeys Date: Thu, 12 Mar 2026 16:40:23 -0400 Subject: [PATCH] feat: implement WebSocket binary protocol and NOT_IMPLEMENTED error code Add CodeNotImplemented error constant (HTTP 501) for planned but unavailable features. Refactor WebSocket packet handling from JSON to binary protocol for improved efficiency: New packet structure: - PacketTypeSuccess (0x00): [type:1][json_data:var] - PacketTypeError (0x01): [type:1][code_len:1][code:var][msg_len:2][msg:var][details_len:2][details:var] - PacketTypeData (0x02): Reserved for future use Update SendErrorPacket: - Build binary error packets with length-prefixed fields - Use WriteMessage with websocket.BinaryMessage Update SendSuccessPacket: - Marshal data to JSON then wrap in binary packet - Eliminates "success" wrapper field for cleaner protocol Add helper functions: - NewNotImplemented(feature) - Standard 501 error - NewNotImplementedWithIssue(feature, issueURL) - 501 with GitHub reference --- internal/api/errors/errors.go | 68 +++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/internal/api/errors/errors.go b/internal/api/errors/errors.go index b42a126..6a126ba 100644 --- a/internal/api/errors/errors.go +++ b/internal/api/errors/errors.go @@ -34,6 +34,7 @@ const ( CodeBadRequest = "BAD_REQUEST" CodeForbidden = "FORBIDDEN" CodeNotFound = "NOT_FOUND" + CodeNotImplemented = "NOT_IMPLEMENTED" ) // ErrorResponse represents a standardized error response @@ -79,21 +80,39 @@ func WriteHTTPErrorFromError(w http.ResponseWriter, statusCode int, err error) { WriteHTTPError(w, statusCode, CodeUnknownError, err.Error(), "") } -// SendErrorPacket sends an error packet over WebSocket +// Packet types for binary WebSocket protocol +const ( + PacketTypeSuccess = 0x00 + PacketTypeError = 0x01 + PacketTypeData = 0x02 +) + +// SendErrorPacket sends an error packet over WebSocket using binary protocol func SendErrorPacket(conn *websocket.Conn, code, message, details string) error { - resp := NewErrorResponse(code, message, details) - return conn.WriteJSON(resp) + // Build binary error packet: [type:1][code_len:1][code:var][msg_len:2][msg:var][details_len:2][details:var] + buf := make([]byte, 0, 1+1+len(code)+2+len(message)+2+len(details)) + buf = append(buf, PacketTypeError) + buf = append(buf, byte(len(code))) + buf = append(buf, []byte(code)...) + buf = append(buf, byte(len(message)>>8), byte(len(message))) + buf = append(buf, []byte(message)...) + buf = append(buf, byte(len(details)>>8), byte(len(details))) + buf = append(buf, []byte(details)...) + return conn.WriteMessage(websocket.BinaryMessage, buf) } -// SendSuccessPacket sends a success packet over WebSocket -// Automatically adds "success": true to the response +// SendSuccessPacket sends a success packet over WebSocket using binary protocol +// The data is serialized as JSON within the binary packet func SendSuccessPacket(conn *websocket.Conn, data map[string]any) error { - response := make(map[string]any, len(data)+1) - response["success"] = true - for k, v := range data { - response[k] = v + // Build binary success packet: [type:1][json_data:var] + jsonData, err := json.Marshal(data) + if err != nil { + return err } - return conn.WriteJSON(response) + buf := make([]byte, 0, 1+len(jsonData)) + buf = append(buf, PacketTypeSuccess) + buf = append(buf, jsonData...) + return conn.WriteMessage(websocket.BinaryMessage, buf) } // Common error responses as typed errors for use with errors.Is/errors.As @@ -104,8 +123,35 @@ var ( ErrInvalidRequest = NewErrorResponse(CodeInvalidRequest, "invalid request", "") ErrServiceUnavailable = NewErrorResponse(CodeServiceUnavailable, "service unavailable", "") ErrServerOverloaded = NewErrorResponse(CodeServerOverloaded, "server overloaded", "") + ErrNotImplemented = NewErrorResponse(CodeNotImplemented, "feature not implemented", "") ) +// NewNotImplemented creates a not implemented error with feature name +func NewNotImplemented(feature string) ErrorResponse { + return NewErrorResponse( + CodeNotImplemented, + fmt.Sprintf("%s is not yet implemented", feature), + "This feature is planned but not yet available. See documentation for alternatives.", + ) +} + +// NewNotImplementedWithIssue creates a not implemented error with GitHub issue reference +func NewNotImplementedWithIssue(feature string, issueURL string) ErrorResponse { + return NewErrorResponse( + CodeNotImplemented, + fmt.Sprintf("%s is not yet implemented", feature), + fmt.Sprintf("Track progress at: %s", issueURL), + ) +} + +// IsNotImplemented checks if error is a not implemented error +func IsNotImplemented(err error) bool { + if errResp, ok := err.(ErrorResponse); ok { + return errResp.Code == CodeNotImplemented + } + return false +} + // HTTPStatusFromCode maps error codes to HTTP status codes func HTTPStatusFromCode(code string) int { switch code { @@ -123,6 +169,8 @@ func HTTPStatusFromCode(code string) int { return http.StatusServiceUnavailable case CodeTimeout: return http.StatusRequestTimeout + case CodeNotImplemented: + return http.StatusNotImplemented default: return http.StatusInternalServerError }