Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions cmd/cycloid/middleware/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ func TestAPIResponseError(t *testing.T) {
assert.Equal(t, "API error 500: 500 Internal Server Error", err.Error())
})

t.Run("ErrorWithoutPayloadFallbackToRawBodyAndPath", func(t *testing.T) {
err := &middleware.APIResponseError{
StatusCode: 422,
Status: "422 Unprocessable Entity",
Path: "/organizations/org/projects/project/environments/env/components",
Body: []byte("stack branch simple-terraform not found"),
}
assert.Equal(
t,
`API error 422 on "/organizations/org/projects/project/environments/env/components": stack branch simple-terraform not found`,
err.Error(),
)
})

t.Run("GetPayload", func(t *testing.T) {
payload := &models.ErrorPayload{}
err := &middleware.APIResponseError{
Expand All @@ -48,4 +62,17 @@ func TestAPIResponseError(t *testing.T) {
}
assert.Equal(t, payload, err.GetPayload())
})

t.Run("ErrorWithoutPayloadFallbackToRawBodyWithoutPath", func(t *testing.T) {
err := &middleware.APIResponseError{
StatusCode: 422,
Status: "422 Unprocessable Entity",
Body: []byte("stack branch simple-terraform not found"),
}
assert.Equal(
t,
"API error 422: stack branch simple-terraform not found",
err.Error(),
)
})
}
26 changes: 20 additions & 6 deletions cmd/cycloid/middleware/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/cycloidio/cycloid-cli/client/models"
)
Expand Down Expand Up @@ -32,13 +33,13 @@ type StackUseCase struct {
// Request represents an HTTP request to the Cycloid API.
type Request struct {
Method string
Organization *string // used for auth token lookup
NoAuth bool // disables auth header
Route []string // joined onto base URL: ["organizations", org, "projects"]
Query any // url.Values or struct with `url` tags
Organization *string // used for auth token lookup
NoAuth bool // disables auth header
Route []string // joined onto base URL: ["organizations", org, "projects"]
Query any // url.Values or struct with `url` tags
Headers map[string]string
Accept *string // overrides default Accept header
Body any // JSON-marshalled when non-nil
Accept *string // overrides default Accept header
Body any // JSON-marshalled when non-nil
}

// APIResponseError is returned when the API returns a non-2xx response.
Expand All @@ -47,12 +48,22 @@ type APIResponseError struct {
Status string
Body []byte
Payload *models.ErrorPayload
Path string
}

func (e *APIResponseError) Error() string {
if e.Payload != nil && len(e.Payload.Errors) > 0 && e.Payload.Errors[0].Message != nil {
return fmt.Sprintf("API error %d: %s", e.StatusCode, *e.Payload.Errors[0].Message)
}

body := strings.TrimSpace(string(e.Body))
if body != "" {
if e.Path != "" {
return fmt.Sprintf("API error %d on %q: %s", e.StatusCode, e.Path, body)
}
return fmt.Sprintf("API error %d: %s", e.StatusCode, body)
}

return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Status)
}

Expand All @@ -67,6 +78,9 @@ func newAPIResponseError(resp *http.Response, body []byte) *APIResponseError {
Status: resp.Status,
Body: body,
}
if resp.Request != nil && resp.Request.URL != nil {
apiErr.Path = resp.Request.URL.RequestURI()
}

var payload models.ErrorPayload
if err := json.Unmarshal(body, &payload); err == nil && len(payload.Errors) > 0 {
Expand Down
27 changes: 27 additions & 0 deletions cmd/cycloid/middleware/http_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package middleware

import (
"net/http"
"net/url"
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewAPIResponseError_PathFallback(t *testing.T) {
u, err := url.Parse("https://http-api.cycloid.io/organizations/org/projects/project?foo=bar")
assert.NoError(t, err)

resp := &http.Response{
StatusCode: http.StatusBadRequest,
Status: "400 Bad Request",
Request: &http.Request{
URL: u,
},
}

apiErr := newAPIResponseError(resp, []byte("raw backend error"))
assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode)
assert.Equal(t, "/organizations/org/projects/project?foo=bar", apiErr.Path)
assert.Equal(t, "API error 400 on \"/organizations/org/projects/project?foo=bar\": raw backend error", apiErr.Error())
}
5 changes: 4 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,12 @@ type APIResponseError struct {
Status string
Body []byte // raw response body
Payload *models.ErrorPayload // parsed if body was valid JSON error
Path string // request path (+ query) for fallback errors
}

// Error() format: "API error 422: <message from payload>"
// Error() format:
// - payload message available: "API error 422: <message from payload>"
// - fallback body/path: "API error 422 on "/path?query": <raw body>"
```

Check with `errors.As`:
Expand Down