Skip to content

Commit d1be6d4

Browse files
committed
fix: improve websocket error handling with proper error classification and actionable messages
Stop infinite retry loops on permanent websocket errors (permission denied, auth failures). Detect error type and show clear, actionable messages instead of raw websocket close frames. Changes: - Add shared error classifier (IsPermissionError, IsPermanentCloseError, IsInternalServerError) - Attempt token refresh once on 1008 auth errors (JWT only, not env tokens) - Gate dial-level refresh on HTTP 401/403 to avoid corrupting stored credentials - Reset refresh guards after successful connection for long-lived sessions - Show minimum required role (Deployer) on permission denied - Show cluster health message on 1011 (treated as transient, retries) - Stop retry loop and exit cleanly on permanent errors (1007, 1008) - Replace log.Fatal with log.Errorf in port-forward (don't kill listener) - Handle SIGINT alongside SIGTERM, Ctrl+C support when not connected - Drain done channel after write/ping errors to capture real close frame
1 parent e973ca6 commit d1be6d4

9 files changed

Lines changed: 451 additions & 53 deletions

File tree

pkg/log.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package pkg
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"github.com/gorilla/websocket"
78
"github.com/qovery/qovery-cli/utils"
@@ -28,9 +29,16 @@ type LogMessage struct {
2829
}
2930

3031
func ExecLog(req *LogRequest) {
31-
wsConn, err := createLogWebsocket(req)
32+
wsConn, resp, err := createLogWebsocket(req)
3233
if err != nil {
33-
log.Fatal("error while creating websocket connection", err)
34+
if !utils.IsUsingEnvToken() && IsAuthDialError(resp) {
35+
if _, refreshErr := utils.ForceRefreshAccessToken(); refreshErr == nil {
36+
wsConn, _, err = createLogWebsocket(req)
37+
}
38+
}
39+
if err != nil {
40+
log.Fatal(ConnectionFailedMessage(err))
41+
}
3442
}
3543
defer func() {
3644
if err := wsConn.Close(); err != nil {
@@ -42,7 +50,14 @@ func ExecLog(req *LogRequest) {
4250
for {
4351
_, msg, err := wsConn.ReadMessage()
4452
if err != nil {
45-
if e, ok := err.(*websocket.CloseError); ok {
53+
if IsPermanentCloseError(err) {
54+
log.Fatal(PermanentErrorMessage(err, "Logs"))
55+
}
56+
if IsInternalServerError(err) {
57+
log.Fatal(ServiceUnavailableMessage("Logs"))
58+
}
59+
var e *websocket.CloseError
60+
if errors.As(err, &e) {
4661
log.Error("connection closed by server: ", e)
4762
return
4863
}
@@ -62,7 +77,7 @@ func ExecLog(req *LogRequest) {
6277
}
6378
}
6479

65-
func createLogWebsocket(req *LogRequest) (*websocket.Conn, error) {
80+
func createLogWebsocket(req *LogRequest) (*websocket.Conn, *http.Response, error) {
6681
wsURL, err := url.Parse(fmt.Sprintf(
6782
"%s/service/logs?service=%s&cluster=%s&environment=%s&organization=%s&project=%s",
6883
utils.WebsocketUrl(),
@@ -73,20 +88,20 @@ func createLogWebsocket(req *LogRequest) (*websocket.Conn, error) {
7388
req.ProjectID,
7489
))
7590
if err != nil {
76-
return nil, err
91+
return nil, nil, err
7792
}
7893

7994
tokenType, token, err := utils.GetAccessToken()
8095
if err != nil {
81-
return nil, err
96+
return nil, nil, err
8297
}
8398

8499
headers := http.Header{"Authorization": {utils.GetAuthorizationHeaderValue(tokenType, token)}}
85-
wsConn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), headers)
100+
wsConn, resp, err := websocket.DefaultDialer.Dial(wsURL.String(), headers)
86101
if err != nil {
87-
return nil, err
102+
return nil, resp, err
88103
}
89-
return wsConn, nil
104+
return wsConn, resp, nil
90105
}
91106

92107
type Timestamp struct {

pkg/port-forward.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ func (w WebsocketPortForward) Read(p []byte) (n int, err error) {
4040
for {
4141
msgType, msg, err := w.ws.ReadMessage()
4242
if err != nil {
43+
if IsPermanentCloseError(err) {
44+
log.Error(PermanentErrorMessage(err, "Port-forward"))
45+
}
4346
return 0, err
4447
}
4548

@@ -55,32 +58,32 @@ func (w WebsocketPortForward) Read(p []byte) (n int, err error) {
5558
}
5659
}
5760

58-
func mkWebsocketConn(req *PortForwardRequest) (*WebsocketPortForward, error) {
61+
func mkWebsocketConn(req *PortForwardRequest) (*WebsocketPortForward, *http.Response, error) {
5962
command, err := query.Values(req)
6063
if err != nil {
61-
return nil, err
64+
return nil, nil, err
6265
}
6366

6467
wsURL, err := url.Parse(fmt.Sprintf("%s/shell/portforward", utils.WebsocketUrl()))
6568
if err != nil {
66-
return nil, err
69+
return nil, nil, err
6770
}
6871
pattern := regexp.MustCompile("%5B([0-9]+)%5D=")
6972
wsURL.RawQuery = pattern.ReplaceAllString(command.Encode(), "[${1}]=")
7073

7174
tokenType, token, err := utils.GetAccessToken()
7275
if err != nil {
73-
return nil, err
76+
return nil, nil, err
7477
}
7578

7679
headers := http.Header{"Authorization": {utils.GetAuthorizationHeaderValue(tokenType, token)}}
77-
wsConn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), headers)
80+
wsConn, resp, err := websocket.DefaultDialer.Dial(wsURL.String(), headers)
7881
if err != nil {
79-
return nil, err
82+
return nil, resp, err
8083
}
8184

8285
ws := WebsocketPortForward{ws: wsConn}
83-
return &ws, nil
86+
return &ws, resp, nil
8487
}
8588

8689
func ExecPortForward(req *PortForwardRequest) {
@@ -122,9 +125,17 @@ func handleConnection(con net.Conn, req *PortForwardRequest) {
122125
}
123126
}()
124127

125-
wsConn, err := mkWebsocketConn(req)
128+
wsConn, resp, err := mkWebsocketConn(req)
126129
if err != nil {
127-
log.Fatal("error while creating websocket connection", err)
130+
if !utils.IsUsingEnvToken() && IsAuthDialError(resp) {
131+
if _, refreshErr := utils.ForceRefreshAccessToken(); refreshErr == nil {
132+
wsConn, _, err = mkWebsocketConn(req)
133+
}
134+
}
135+
if err != nil {
136+
log.Error(ConnectionFailedMessage(err))
137+
return
138+
}
128139
}
129140
defer func() {
130141
if err := wsConn.ws.Close(); err != nil {

0 commit comments

Comments
 (0)