loader revisited
This commit is contained in:
+63
-10
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"galaxy/connector"
|
||||
gerr "galaxy/error"
|
||||
"galaxy/model/client"
|
||||
"galaxy/model/report"
|
||||
"io"
|
||||
@@ -59,7 +60,7 @@ type httpConnector struct {
|
||||
func NewHttpConnector(ctx context.Context, backendURL string) (*httpConnector, error) {
|
||||
u, err := url.Parse(backendURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, gerr.WrapService(fmt.Errorf("parse backend URL %q: %w", backendURL, err))
|
||||
}
|
||||
h := &httpConnector{
|
||||
ctx: ctx,
|
||||
@@ -162,6 +163,58 @@ func isConnectTimeout(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isConnectionError reports transport-level connectivity failures that should
|
||||
// be surfaced as connection errors instead of service contract errors.
|
||||
func isConnectionError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return false
|
||||
}
|
||||
if isConnectTimeout(err) {
|
||||
return true
|
||||
}
|
||||
|
||||
var urlErr *url.Error
|
||||
if errors.As(err, &urlErr) {
|
||||
err = urlErr.Err
|
||||
}
|
||||
|
||||
var dnsErr *net.DNSError
|
||||
if errors.As(err, &dnsErr) {
|
||||
return true
|
||||
}
|
||||
|
||||
var opErr *net.OpError
|
||||
if errors.As(err, &opErr) {
|
||||
return true
|
||||
}
|
||||
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func classifyConnectorError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
if gerr.IsConnection(err) || gerr.IsService(err) {
|
||||
return err
|
||||
}
|
||||
if isConnectionError(err) {
|
||||
return gerr.WrapConnection(err)
|
||||
}
|
||||
return gerr.WrapService(err)
|
||||
}
|
||||
|
||||
// CheckConnection probes backend status endpoint and reports whether server is reachable.
|
||||
func (h *httpConnector) CheckConnection() bool {
|
||||
resp, err := h.doRequest(h.requestContext(), checkConnectionPath)
|
||||
@@ -177,17 +230,17 @@ func (h *httpConnector) CheckConnection() bool {
|
||||
func (h *httpConnector) CheckVersion() ([]connector.VersionInfo, error) {
|
||||
resp, err := h.doRequest(h.requestContext(), checkVersionPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request versions from backend: %w", err)
|
||||
return nil, classifyConnectorError(fmt.Errorf("request versions from backend: %w", err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("request versions from backend: unexpected status code %d", resp.StatusCode)
|
||||
return nil, classifyConnectorError(fmt.Errorf("request versions from backend: unexpected status code %d", resp.StatusCode))
|
||||
}
|
||||
|
||||
var versions []connector.VersionInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil {
|
||||
return nil, fmt.Errorf("decode versions response: %w", err)
|
||||
return nil, classifyConnectorError(fmt.Errorf("decode versions response: %w", err))
|
||||
}
|
||||
|
||||
return versions, nil
|
||||
@@ -198,17 +251,17 @@ func (h *httpConnector) CheckVersion() ([]connector.VersionInfo, error) {
|
||||
func (h *httpConnector) DownloadVersion(urlOrPath string) ([]byte, error) {
|
||||
resp, err := h.doRequest(h.requestContext(), urlOrPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download version artifact: %w", err)
|
||||
return nil, classifyConnectorError(fmt.Errorf("download version artifact: %w", err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("download version artifact: unexpected status code %d", resp.StatusCode)
|
||||
return nil, classifyConnectorError(fmt.Errorf("download version artifact: unexpected status code %d", resp.StatusCode))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read version artifact body: %w", err)
|
||||
return nil, classifyConnectorError(fmt.Errorf("read version artifact body: %w", err))
|
||||
}
|
||||
|
||||
return body, nil
|
||||
@@ -228,17 +281,17 @@ func (h *httpConnector) FetchReport(_ client.GameID, turn uint, callback func(re
|
||||
func (h *httpConnector) fetchReport(turn uint) (report.Report, error) {
|
||||
resp, err := h.doRequest(h.requestContext(), fetchReportRequestPath(turn))
|
||||
if err != nil {
|
||||
return report.Report{}, fmt.Errorf("request report from backend: %w", err)
|
||||
return report.Report{}, classifyConnectorError(fmt.Errorf("request report from backend: %w", err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return report.Report{}, fmt.Errorf("request report from backend: unexpected status code %d", resp.StatusCode)
|
||||
return report.Report{}, classifyConnectorError(fmt.Errorf("request report from backend: unexpected status code %d", resp.StatusCode))
|
||||
}
|
||||
|
||||
var rep report.Report
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rep); err != nil {
|
||||
return report.Report{}, fmt.Errorf("decode report response: %w", err)
|
||||
return report.Report{}, classifyConnectorError(fmt.Errorf("decode report response: %w", err))
|
||||
}
|
||||
|
||||
return rep, nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"galaxy/connector"
|
||||
gerr "galaxy/error"
|
||||
"galaxy/model/report"
|
||||
"io"
|
||||
"net"
|
||||
@@ -151,12 +152,12 @@ func TestCheckVersion(t *testing.T) {
|
||||
t,
|
||||
context.Background(),
|
||||
stdhttp.StatusOK,
|
||||
`[{"os":"darwin","version":"1.2.3","url":"https://example.com/darwin"}]`,
|
||||
`[{"os":"darwin","arch":"amd64","kind":"executable","version":"1.2.3","url":"https://example.com/darwin"}]`,
|
||||
"",
|
||||
)
|
||||
},
|
||||
want: []connector.VersionInfo{
|
||||
{OS: "darwin", Version: "1.2.3", URL: "https://example.com/darwin"},
|
||||
{OS: "darwin", Arch: "amd64", Kind: connector.ArtifactKindExecutable, Version: "1.2.3", URL: "https://example.com/darwin"},
|
||||
},
|
||||
wantPath: "/api/v1/versions",
|
||||
},
|
||||
@@ -215,12 +216,12 @@ func TestCheckVersion(t *testing.T) {
|
||||
t,
|
||||
context.Background(),
|
||||
stdhttp.StatusOK,
|
||||
`[{"os":"linux","version":"2.0.0","url":"https://example.com/linux"}]`,
|
||||
`[{"os":"linux","arch":"amd64","kind":"executable","version":"2.0.0","url":"https://example.com/linux"}]`,
|
||||
"/base",
|
||||
)
|
||||
},
|
||||
want: []connector.VersionInfo{
|
||||
{OS: "linux", Version: "2.0.0", URL: "https://example.com/linux"},
|
||||
{OS: "linux", Arch: "amd64", Kind: connector.ArtifactKindExecutable, Version: "2.0.0", URL: "https://example.com/linux"},
|
||||
},
|
||||
wantPath: "/base/api/v1/versions",
|
||||
},
|
||||
@@ -260,6 +261,65 @@ func TestCheckVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckVersionClassifiesTransportFailure verifies transport failures are surfaced as connection errors.
|
||||
func TestCheckVersionClassifiesTransportFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := newUnreachableConnector(t, context.Background())
|
||||
|
||||
_, err := conn.CheckVersion()
|
||||
if err == nil {
|
||||
t.Fatal("CheckVersion() error = nil, want non-nil")
|
||||
}
|
||||
if !gerr.IsConnection(err) {
|
||||
t.Fatalf("CheckVersion() error = %v, want connection classified error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckVersionClassifiesInvalidJSON verifies malformed backend payloads are surfaced as service errors.
|
||||
func TestCheckVersionClassifiesInvalidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn, _ := newVersionServerConnector(
|
||||
t,
|
||||
context.Background(),
|
||||
stdhttp.StatusOK,
|
||||
`{"versions":`,
|
||||
"",
|
||||
)
|
||||
|
||||
_, err := conn.CheckVersion()
|
||||
if err == nil {
|
||||
t.Fatal("CheckVersion() error = nil, want non-nil")
|
||||
}
|
||||
if !gerr.IsService(err) {
|
||||
t.Fatalf("CheckVersion() error = %v, want service classified error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDownloadVersionClassifiesUnexpectedStatus verifies HTTP protocol failures are surfaced as service errors.
|
||||
func TestDownloadVersionClassifiesUnexpectedStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
w.WriteHeader(stdhttp.StatusBadGateway)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
conn, err := NewHttpConnector(context.Background(), server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHttpConnector() error = %v", err)
|
||||
}
|
||||
|
||||
_, err = conn.DownloadVersion("downloads/client.bin")
|
||||
if err == nil {
|
||||
t.Fatal("DownloadVersion() error = nil, want non-nil")
|
||||
}
|
||||
if !gerr.IsService(err) {
|
||||
t.Fatalf("DownloadVersion() error = %v, want service classified error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFetchReport verifies asynchronous report retrieval behavior.
|
||||
func TestFetchReport(t *testing.T) {
|
||||
tests := []fetchReportCase{
|
||||
|
||||
Reference in New Issue
Block a user