detached 2b58860

fix(cli) narrow retry to gateway errors and warn on insecure TLS

commit
2b588606410eef052b591d62e818045be7db4491
author
bnema · · 2026-02-06 20:43 UTC
parent
fce0810
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +12 -5
M internal/adapters/in/cli/remote/client.go +9 -2
@@ -11,6 +11,7 @@ import (
1111 "io"
1212 "net/http"
1313 "net/url"
14+ "os"
1415 "strings"
1516 "time"
1617
@@ -88,6 +89,8 @@ func (c *Client) applyTLSConfig() {
8889 return
8990 }
9091
92+ fmt.Fprintf(os.Stderr, "WARNING: TLS certificate verification disabled for %s\n", c.baseURL)
93+
9194 var transport *http.Transport
9295 if existing, ok := c.httpClient.Transport.(*http.Transport); ok {
9396 transport = existing.Clone()
@@ -96,7 +99,9 @@ func (c *Client) applyTLSConfig() {
9699 }
97100
98101 if transport.TLSClientConfig == nil {
99- transport.TLSClientConfig = &tls.Config{}
102+ transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
103+ } else {
104+ transport.TLSClientConfig.MinVersion = tls.VersionTLS12
100105 }
101106 //nolint:gosec // Explicit CLI opt-in via --insecure for self-signed/private cert deployments.
102107 transport.TLSClientConfig.InsecureSkipVerify = true
@@ -164,7 +169,9 @@ func isRetryableRequestError(err error) bool {
164169 }
165170
166171 func isRetryableStatus(status int) bool {
167- return status >= 500 && status <= 599
172+ // Only retry gateway errors; retrying 500 on non-idempotent POSTs
173+ // (deploy, restart, reload) can cause duplicate state changes.
174+ return status == 502 || status == 503 || status == 504
168175 }
169176
170177 func retryDelay(attempt int) time.Duration {
@@ -11,6 +11,7 @@ import (
11 "io"11 "io"
12 "net/http"12 "net/http"
13 "net/url"13 "net/url"
14+ "os"
14 "strings"15 "strings"
15 "time"16 "time"
16 17
@@ -88,6 +89,8 @@ func (c *Client) applyTLSConfig() {
88 return89 return
89 }90 }
90 91
92+ fmt.Fprintf(os.Stderr, "WARNING: TLS certificate verification disabled for %s\n", c.baseURL)
93+
91 var transport *http.Transport94 var transport *http.Transport
92 if existing, ok := c.httpClient.Transport.(*http.Transport); ok {95 if existing, ok := c.httpClient.Transport.(*http.Transport); ok {
93 transport = existing.Clone()96 transport = existing.Clone()
@@ -96,7 +99,9 @@ func (c *Client) applyTLSConfig() {
96 }99 }
97 100
98 if transport.TLSClientConfig == nil {101 if transport.TLSClientConfig == nil {
99- transport.TLSClientConfig = &tls.Config{}102+ transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
103+ } else {
104+ transport.TLSClientConfig.MinVersion = tls.VersionTLS12
100 }105 }
101 //nolint:gosec // Explicit CLI opt-in via --insecure for self-signed/private cert deployments.106 //nolint:gosec // Explicit CLI opt-in via --insecure for self-signed/private cert deployments.
102 transport.TLSClientConfig.InsecureSkipVerify = true107 transport.TLSClientConfig.InsecureSkipVerify = true
@@ -164,7 +169,9 @@ func isRetryableRequestError(err error) bool {
164 }169 }
165 170
166 func isRetryableStatus(status int) bool {171 func isRetryableStatus(status int) bool {
167- return status >= 500 && status <= 599172+ // Only retry gateway errors; retrying 500 on non-idempotent POSTs
173+ // (deploy, restart, reload) can cause duplicate state changes.
174+ return status == 502 || status == 503 || status == 504
168 }175 }
169 176
170 func retryDelay(attempt int) time.Duration {177 func retryDelay(attempt int) time.Duration {
M internal/adapters/in/cli/remote/client_test.go +3 -3
@@ -82,15 +82,15 @@ func TestClientDeployReturnsErrorAfterRetryExhaustion(t *testing.T) {
8282 require.Equal(t, "/admin/deploy/test.example.com", r.URL.Path)
8383 require.Equal(t, http.MethodPost, r.Method)
8484 atomic.AddInt32(&attempts, 1)
85- w.WriteHeader(http.StatusInternalServerError)
86- _, _ = w.Write([]byte(`{"error":"failed to deploy container"}`))
85+ w.WriteHeader(http.StatusBadGateway)
86+ _, _ = w.Write([]byte(`{"error":"upstream unavailable"}`))
8787 }))
8888 defer srv.Close()
8989
9090 client := NewClient(srv.URL)
9191 _, err := client.Deploy(context.Background(), "test.example.com")
9292 require.Error(t, err)
93- assert.Contains(t, err.Error(), "500 Internal Server Error: failed to deploy container")
93+ assert.Contains(t, err.Error(), "502 Bad Gateway: upstream unavailable")
9494 assert.EqualValues(t, retryMaxAttempts, atomic.LoadInt32(&attempts))
9595 }
9696
@@ -82,15 +82,15 @@ func TestClientDeployReturnsErrorAfterRetryExhaustion(t *testing.T) {
82 require.Equal(t, "/admin/deploy/test.example.com", r.URL.Path)82 require.Equal(t, "/admin/deploy/test.example.com", r.URL.Path)
83 require.Equal(t, http.MethodPost, r.Method)83 require.Equal(t, http.MethodPost, r.Method)
84 atomic.AddInt32(&attempts, 1)84 atomic.AddInt32(&attempts, 1)
85- w.WriteHeader(http.StatusInternalServerError)85+ w.WriteHeader(http.StatusBadGateway)
86- _, _ = w.Write([]byte(`{"error":"failed to deploy container"}`))86+ _, _ = w.Write([]byte(`{"error":"upstream unavailable"}`))
87 }))87 }))
88 defer srv.Close()88 defer srv.Close()
89 89
90 client := NewClient(srv.URL)90 client := NewClient(srv.URL)
91 _, err := client.Deploy(context.Background(), "test.example.com")91 _, err := client.Deploy(context.Background(), "test.example.com")
92 require.Error(t, err)92 require.Error(t, err)
93- assert.Contains(t, err.Error(), "500 Internal Server Error: failed to deploy container")93+ assert.Contains(t, err.Error(), "502 Bad Gateway: upstream unavailable")
94 assert.EqualValues(t, retryMaxAttempts, atomic.LoadInt32(&attempts))94 assert.EqualValues(t, retryMaxAttempts, atomic.LoadInt32(&attempts))
95 }95 }
96 96

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close