Skip to content

Commit 59773c1

Browse files
committed
ssh: actionable error when binary upload is reset by network proxy
When `databricks ssh connect` uploads the CLI binary to the workspace, a network intermediary (corporate egress proxy, VPN, firewall/WAF) may close the HTTP/2 stream mid-upload, surfacing as a cryptic `stream error: stream ID N; NO_ERROR; received from peer`. Detect this transport-level reset and wrap the error with a hint that the connection was closed by an intermediary and the user should try from a network without such restrictions. Co-authored-by: Isaac
1 parent 37738c3 commit 59773c1

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

experimental/ssh/internal/client/releases.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/databricks/cli/libs/filer"
1616
"github.com/databricks/cli/libs/log"
1717
"github.com/databricks/databricks-sdk-go"
18+
"golang.org/x/net/http2"
1819
)
1920

2021
type releaseProvider func(ctx context.Context, architecture, version, releasesDir string) (io.ReadCloser, error)
@@ -65,6 +66,14 @@ func uploadReleases(ctx context.Context, workspaceFiler filer.Filer, getRelease
6566
// producing the filerRoot/remoteSubFolder/*archive-contents* structure, with 'databricks' binary inside.
6667
err = workspaceFiler.Write(ctx, remoteArchivePath, releaseReader, filer.OverwriteIfExists, filer.CreateParentDirectories)
6768
if err != nil {
69+
if isStreamResetError(err) {
70+
return fmt.Errorf("failed to upload file %s to workspace: %w\n\n"+
71+
"The connection was closed before the upload finished. "+
72+
"This is usually caused by a network intermediary (corporate egress proxy, VPN, or firewall/WAF) "+
73+
"enforcing a request-body size limit on POSTs to *.cloud.databricks.com. "+
74+
"Try running this command from a network without such restrictions.",
75+
remoteArchivePath, err)
76+
}
6877
return fmt.Errorf("failed to upload file %s to workspace: %w", remoteArchivePath, err)
6978
}
7079
log.Infof(ctx, "Successfully uploaded %s to workspace", remoteBinaryPath)
@@ -73,6 +82,18 @@ func uploadReleases(ctx context.Context, workspaceFiler filer.Filer, getRelease
7382
return nil
7483
}
7584

85+
// isStreamResetError reports whether err looks like an HTTP/2 stream reset from
86+
// the server, which typically means an edge proxy or the workspace-files import
87+
// endpoint rejected the request body (e.g. body-size limit).
88+
func isStreamResetError(err error) bool {
89+
var se http2.StreamError
90+
if errors.As(err, &se) {
91+
return true
92+
}
93+
msg := err.Error()
94+
return strings.Contains(msg, "stream error") && strings.Contains(msg, "stream ID")
95+
}
96+
7697
func getReleaseName(architecture, version string) string {
7798
if strings.Contains(version, "dev") {
7899
return fmt.Sprintf("databricks_cli_linux_%s.zip", architecture)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package client
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"golang.org/x/net/http2"
10+
)
11+
12+
func TestIsStreamResetError(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
err error
16+
want bool
17+
}{
18+
{
19+
name: "http2 stream error type",
20+
err: http2.StreamError{StreamID: 15, Code: http2.ErrCodeNo},
21+
want: true,
22+
},
23+
{
24+
name: "wrapped http2 stream error type",
25+
err: fmt.Errorf("post failed: %w", http2.StreamError{StreamID: 15, Code: http2.ErrCodeNo}),
26+
want: true,
27+
},
28+
{
29+
name: "string match from peer reset (Go HTTP/2 client format)",
30+
err: errors.New(`Post "https://example/api/2.0/workspace-files/import-file/...": stream error: stream ID 15; NO_ERROR; received from peer`),
31+
want: true,
32+
},
33+
{
34+
name: "unrelated error",
35+
err: errors.New("connection refused"),
36+
want: false,
37+
},
38+
{
39+
name: "API error message",
40+
err: errors.New("RESOURCE_DOES_NOT_EXIST: path does not exist"),
41+
want: false,
42+
},
43+
}
44+
45+
for _, tt := range tests {
46+
t.Run(tt.name, func(t *testing.T) {
47+
assert.Equal(t, tt.want, isStreamResetError(tt.err))
48+
})
49+
}
50+
}

0 commit comments

Comments
 (0)