Skip to content

Commit 546173d

Browse files
committed
Try unify error response parsing and handling
1 parent 99a1e89 commit 546173d

File tree

9 files changed

+455
-687
lines changed

9 files changed

+455
-687
lines changed

internal/tiger/api/client.go

Lines changed: 120 additions & 232 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/tiger/api/client_util.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"io"
910
"net/http"
1011
"sync"
1112
"time"
@@ -134,3 +135,72 @@ func FormatAPIErrorFromBody(body []byte, fallback string) error {
134135
}
135136
return errors.New(fallback)
136137
}
138+
139+
// Error implements the error interface for the Error type.
140+
// This allows Error values to be used directly as Go errors.
141+
func (e *Error) Error() string {
142+
if e == nil {
143+
return "unknown error"
144+
}
145+
if e.Message != nil && *e.Message != "" {
146+
return *e.Message
147+
}
148+
return "unknown error"
149+
}
150+
151+
// ResponseError wraps an API response with an error, preserving the status code
152+
// for proper exit code mapping in CLI commands
153+
type ResponseError struct {
154+
statusCode int
155+
response *http.Response
156+
err error
157+
}
158+
159+
func (r *ResponseError) Error() string {
160+
return r.err.Error()
161+
}
162+
163+
func (r *ResponseError) StatusCode() int {
164+
return r.statusCode
165+
}
166+
167+
func (r *ResponseError) HTTPResponse() *http.Response {
168+
return r.response
169+
}
170+
171+
func (r *ResponseError) Unwrap() error {
172+
return r.err
173+
}
174+
175+
// NewResponseError creates an error from an API response
176+
// It attempts to extract the error message from the response body
177+
func NewResponseError(response *http.Response) error {
178+
if response == nil {
179+
return errors.New("unknown error")
180+
}
181+
182+
var err error
183+
bodyBytes, readErr := io.ReadAll(response.Body)
184+
err = errors.New(string(bodyBytes))
185+
return &ResponseError{
186+
statusCode: response.StatusCode,
187+
response: response,
188+
err: err,
189+
}
190+
if readErr == nil && len(bodyBytes) > 0 {
191+
var apiErr Error
192+
if unmarshalErr := json.Unmarshal(bodyBytes, &apiErr); unmarshalErr == nil {
193+
err = &apiErr
194+
} else {
195+
err = fmt.Errorf("API error: status %d", unmarshalErr.Error())
196+
}
197+
} else {
198+
err = fmt.Errorf("API error: status %d", response.StatusCode)
199+
}
200+
201+
return &ResponseError{
202+
statusCode: response.StatusCode,
203+
response: response,
204+
err: err,
205+
}
206+
}

internal/tiger/api/types.go

Lines changed: 2 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/tiger/cmd/db.go

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -321,27 +321,11 @@ func getServiceDetails(cmd *cobra.Command, args []string) (api.Service, error) {
321321
}
322322

323323
// Handle API response
324-
switch resp.StatusCode() {
325-
case 200:
326-
if resp.JSON200 == nil {
327-
return api.Service{}, fmt.Errorf("empty response from API")
328-
}
329-
330-
return *resp.JSON200, nil
331-
332-
case 401:
333-
return api.Service{}, exitWithCode(ExitAuthenticationError, api.FormatAPIErrorFromBody(resp.Body, "authentication failed: invalid API key"))
334-
case 403:
335-
return api.Service{}, exitWithCode(ExitPermissionDenied, api.FormatAPIErrorFromBody(resp.Body, "permission denied: insufficient access to service"))
336-
case 404:
337-
return api.Service{}, exitWithCode(ExitServiceNotFound, api.FormatAPIError(resp.JSON404, fmt.Sprintf("service '%s' not found in project '%s'", serviceID, projectID)))
338-
default:
339-
statusCode := resp.StatusCode()
340-
if statusCode >= 400 && statusCode < 500 {
341-
return api.Service{}, api.FormatAPIErrorFromBody(resp.Body, fmt.Sprintf("API request failed with status %d", statusCode))
342-
}
343-
return api.Service{}, fmt.Errorf("API request failed with status %d", statusCode)
324+
if resp.StatusCode() != 200 {
325+
return api.Service{}, exitWithErrorFromStatusCode(resp.StatusCode(), resp.JSON4XX)
344326
}
327+
328+
return *resp.JSON200, nil
345329
}
346330

347331
// ArgsLenAtDashProvider defines the interface for getting ArgsLenAtDash

internal/tiger/cmd/errors.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package cmd
22

3+
import "errors"
4+
35
// Exit codes as defined in the CLI specification
46
const (
57
ExitSuccess = 0 // Success
@@ -32,3 +34,34 @@ func (e exitCodeError) ExitCode() int {
3234
func exitWithCode(code int, err error) error {
3335
return exitCodeError{code: code, err: err}
3436
}
37+
38+
// exitWithErrorFromStatusCode maps HTTP status codes to CLI exit codes
39+
func exitWithErrorFromStatusCode(statusCode int, err error) error {
40+
if err == nil {
41+
err = errors.New("unknown error")
42+
}
43+
switch statusCode {
44+
case 400:
45+
// Bad request - invalid parameters
46+
return exitWithCode(ExitInvalidParameters, err)
47+
case 401:
48+
// Unauthorized - authentication error
49+
return exitWithCode(ExitAuthenticationError, err)
50+
case 403:
51+
// Forbidden - permission denied
52+
return exitWithCode(ExitPermissionDenied, err)
53+
case 404:
54+
// Not found - service/resource not found
55+
return exitWithCode(ExitServiceNotFound, err)
56+
case 408, 504:
57+
// Request timeout or gateway timeout
58+
return exitWithCode(ExitTimeout, err)
59+
default:
60+
// For other 4xx errors, use general error
61+
if statusCode >= 400 && statusCode < 500 {
62+
return exitWithCode(ExitGeneralError, err)
63+
}
64+
// For 5xx and other errors, use general error
65+
return exitWithCode(ExitGeneralError, err)
66+
}
67+
}

0 commit comments

Comments
 (0)