From 48a12551aa32cdbf19224c1aaf3de2639b0c2d2b Mon Sep 17 00:00:00 2001 From: ayn2op Date: Tue, 29 Sep 2026 01:19:37 -0400 Subject: [PATCH] fix(qr): close the gateway connection on errors --- internal/ui/login/model.go | 6 ++++-- internal/ui/login/qr/conn_test.go | 28 ++++++++++++++++++++++++++++ internal/ui/login/qr/msg.go | 9 ++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/internal/ui/login/model.go b/internal/ui/login/model.go index a0a57a5..3fb839b 100644 --- a/internal/ui/login/model.go +++ b/internal/ui/login/model.go @@ -79,12 +79,14 @@ func (m Model) View() tview.Element { } func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { + var errCmd tview.Cmd switch msg := msg.(type) { case selectTabMsg: m.active = tab(msg) return m, m.Init() case error: - return m, showErrorDialog(msg) + // The active tab still gets the error, so the QR tab can close its connection. + errCmd = showErrorDialog(msg) case copyErrorMsg: return m, setClipboard(string(msg)) } @@ -98,7 +100,7 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { case tokenTab: m.token, cmd = m.token.Update(msg) } - return m, cmd + return m, tview.Batch(errCmd, cmd) } func showErrorDialog(err error) tview.Cmd { diff --git a/internal/ui/login/qr/conn_test.go b/internal/ui/login/qr/conn_test.go index 2c9d9b0..3bd9d2e 100644 --- a/internal/ui/login/qr/conn_test.go +++ b/internal/ui/login/qr/conn_test.go @@ -105,3 +105,31 @@ func TestModelGateway(t *testing.T) { } }) } + +func TestCloseConn(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := (&websocket.Upgrader{}).Upgrade(w, r, nil) + if err != nil { + t.Error(err) + return + } + defer conn.Close() + _, _, _ = conn.ReadMessage() + })) + t.Cleanup(server.Close) + ws, _, err := websocket.DefaultDialer.Dial("ws"+strings.TrimPrefix(server.URL, "http"), nil) + if err != nil { + t.Fatal(err) + } + conn := &gatewayConn{ws: ws} + + // Closing twice must not error, or the error case would close it again forever. + for range 2 { + if msg := closeConn(conn)(); msg != (connCloseMsg{}) { + t.Fatalf("closeConn() = %#v, want connCloseMsg", msg) + } + } + if msg := listen(conn)(); msg != nil { + t.Fatalf("listen() after close = %#v, want nil", msg) + } +} diff --git a/internal/ui/login/qr/msg.go b/internal/ui/login/qr/msg.go index 7a9c273..88d5aa6 100644 --- a/internal/ui/login/qr/msg.go +++ b/internal/ui/login/qr/msg.go @@ -8,6 +8,8 @@ import ( "encoding/base64" json "encoding/json/v2" "errors" + "log/slog" + "net" "strings" "sync" "time" @@ -57,9 +59,10 @@ func connect() tview.Cmd { func closeConn(conn *gatewayConn) tview.Cmd { return func() tview.Msg { + // An error here must not reach the error case, which would close the connection again. if conn != nil { if err := conn.ws.Close(); err != nil { - return err + slog.Error("failed to close remote auth gateway connection", "err", err) } } return connCloseMsg{} @@ -99,6 +102,10 @@ func listen(conn *gatewayConn) tview.Cmd { } _, data, err := conn.ws.ReadMessage() + if errors.Is(err, net.ErrClosed) { + // closeConn closed the connection. + return nil + } if err != nil { return err }