Skip to content

Commit cd24af1

Browse files
authored
Merge pull request #131 from deploymenttheory/dev
Remove unused test file and update error handling in response package
2 parents 4f3dbed + 257d0cc commit cd24af1

File tree

3 files changed

+342
-99
lines changed

3 files changed

+342
-99
lines changed

response/error.go

+165-99
Original file line numberDiff line numberDiff line change
@@ -3,145 +3,211 @@
33
package response
44

55
import (
6+
"bytes"
67
"encoding/json"
8+
"encoding/xml"
79
"fmt"
810
"io"
911
"net/http"
1012
"strings"
1113

1214
"github.com/deploymenttheory/go-api-http-client/logger"
15+
"golang.org/x/net/html"
1316
)
1417

15-
// APIError represents a more flexible structure for API error responses.
18+
// APIError represents an api error response.
1619
type APIError struct {
17-
StatusCode int // HTTP status code
18-
Type string // A brief identifier for the type of error
19-
Message string // Human-readable message
20-
Detail string // Detailed error message
21-
Errors map[string]interface{} // A map to hold various error fields
22-
Raw string // Raw response body for unstructured errors
20+
StatusCode int `json:"status_code" xml:"StatusCode"` // HTTP status code
21+
Type string `json:"type" xml:"Type"` // Type of error
22+
Message string `json:"message" xml:"Message"` // Human-readable message
23+
Detail string `json:"detail,omitempty" xml:"Detail,omitempty"` // Detailed error message
24+
Errors map[string]interface{} `json:"errors,omitempty" xml:"Errors,omitempty"` // Additional error details
25+
Raw string `json:"raw" xml:"Raw"` // Raw response body for debugging
2326
}
2427

25-
// StructuredError represents a structured error response from the API.
26-
type StructuredError struct {
27-
Error struct {
28-
Code string `json:"code"`
29-
Message string `json:"message"`
30-
} `json:"error"`
31-
}
32-
33-
// Error returns a JSON representation of the APIError.
28+
// Error returns a string representation of the APIError, making it compatible with the error interface.
3429
func (e *APIError) Error() string {
30+
// Attempt to marshal the APIError instance into a JSON string.
3531
data, err := json.Marshal(e)
36-
if err != nil {
37-
return fmt.Sprintf("Error encoding APIError to JSON: %s", err)
32+
if err == nil {
33+
return string(data)
34+
}
35+
36+
// Use the standard HTTP status text as the error message if 'Message' field is empty.
37+
if e.Message == "" {
38+
e.Message = http.StatusText(e.StatusCode)
3839
}
39-
return string(data)
40+
41+
// Fallback to a simpler error message format if JSON marshaling fails.
42+
return fmt.Sprintf("API Error: StatusCode=%d, Type=%s, Message=%s", e.StatusCode, e.Type, e.Message)
4043
}
4144

42-
// HandleAPIErrorResponse attempts to parse the error response from the API and logs using the zap logger.
45+
// HandleAPIErrorResponse handles the HTTP error response from an API and logs the error.
4346
func HandleAPIErrorResponse(resp *http.Response, log logger.Logger) *APIError {
4447
apiError := &APIError{
4548
StatusCode: resp.StatusCode,
46-
Type: "APIError", // Default error type
47-
Message: "An error occurred", // Default error message
49+
Type: "APIError",
50+
Message: "An error occurred",
4851
}
4952

5053
bodyBytes, err := io.ReadAll(resp.Body)
5154
if err != nil {
55+
apiError.Raw = "Failed to read response body"
56+
logError(log, apiError, "error_reading_response_body", resp)
5257
return apiError
5358
}
5459

55-
// Check if the response is JSON
56-
if isJSONResponse(resp) {
57-
// Attempt to parse the response into a StructuredError
58-
if err := json.Unmarshal(bodyBytes, &apiError); err == nil && apiError.Message != "" {
59-
log.LogError(
60-
"json_structured_error_detected", // event
61-
resp.Request.Method, // method
62-
resp.Request.URL.String(), // url
63-
resp.StatusCode, // statusCode
64-
resp.Status, // status
65-
fmt.Errorf(apiError.Message), // err
66-
apiError.Raw, // raw resp
67-
)
68-
return apiError
69-
}
60+
mimeType, _ := ParseContentTypeHeader(resp.Header.Get("Content-Type"))
61+
switch mimeType {
62+
case "application/json":
63+
parseJSONResponse(bodyBytes, apiError, log, resp)
64+
logError(log, apiError, "json_error_detected", resp)
65+
case "application/xml", "text/xml":
66+
parseXMLResponse(bodyBytes, apiError, log, resp)
67+
logError(log, apiError, "xml_error_detected", resp)
68+
case "text/html":
69+
parseHTMLResponse(bodyBytes, apiError, log, resp)
70+
logError(log, apiError, "html_error_detected", resp)
71+
case "text/plain":
72+
parseTextResponse(bodyBytes, apiError, log, resp)
73+
logError(log, apiError, "text_error_detected", resp)
74+
default:
75+
apiError.Raw = string(bodyBytes)
76+
apiError.Message = "Unknown content type error"
77+
logError(log, apiError, "unknown_content_type_error", resp)
78+
}
79+
80+
return apiError
81+
}
7082

71-
// If structured parsing fails, attempt to parse into a generic error map
72-
var genericErr map[string]interface{}
73-
if err := json.Unmarshal(bodyBytes, &genericErr); err == nil {
74-
apiError.updateFromGenericError(genericErr)
75-
log.LogError(
76-
"json_generic_error_detected", // event
77-
resp.Request.Method, // method
78-
resp.Request.URL.String(), // url
79-
resp.StatusCode, // statusCode
80-
resp.Status, // status
81-
fmt.Errorf(apiError.Message), // err
82-
apiError.Raw, // raw resp
83-
)
84-
return apiError
83+
// ParseContentTypeHeader parses the Content-Type header and extracts the MIME type and parameters.
84+
func ParseContentTypeHeader(header string) (string, map[string]string) {
85+
parts := strings.Split(header, ";")
86+
mimeType := strings.TrimSpace(parts[0])
87+
params := make(map[string]string)
88+
for _, part := range parts[1:] {
89+
kv := strings.SplitN(part, "=", 2)
90+
if len(kv) == 2 {
91+
params[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
8592
}
86-
} else if isHTMLResponse(resp) {
87-
// Handle HTML response
93+
}
94+
return mimeType, params
95+
}
96+
97+
// parseJSONResponse attempts to parse the JSON error response and update the APIError structure.
98+
func parseJSONResponse(bodyBytes []byte, apiError *APIError, log logger.Logger, resp *http.Response) {
99+
if err := json.Unmarshal(bodyBytes, apiError); err != nil {
88100
apiError.Raw = string(bodyBytes)
89-
log.LogError(
90-
"api_html_error", // event
91-
resp.Request.Method, // method
92-
resp.Request.URL.String(), // url
93-
resp.StatusCode, // statusCode
94-
resp.Status, // status
95-
fmt.Errorf(apiError.Message), // err
96-
apiError.Raw, // raw resp
101+
log.LogError("json_parsing_error",
102+
resp.Request.Method,
103+
resp.Request.URL.String(),
104+
resp.StatusCode,
105+
"JSON parsing failed",
106+
err,
107+
apiError.Raw,
97108
)
98-
return apiError
99109
} else {
100-
// Handle other non-JSON responses
110+
// Successfully parsed JSON error, so log the error details.
111+
logError(log, apiError, "json_error_detected", resp)
112+
}
113+
}
114+
115+
// parseXMLResponse should be implemented to parse XML responses and log errors using the centralized logger.
116+
func parseXMLResponse(bodyBytes []byte, apiError *APIError, log logger.Logger, resp *http.Response) {
117+
var xmlErr APIError
118+
119+
// Attempt to unmarshal the XML body into the XMLErrorResponse struct
120+
if err := xml.Unmarshal(bodyBytes, &xmlErr); err != nil {
121+
// If parsing fails, log the error and keep the raw response
101122
apiError.Raw = string(bodyBytes)
102-
log.LogError(
103-
"api_non_json_error", // event
104-
resp.Request.Method, // method
105-
resp.Request.URL.String(), // url
106-
resp.StatusCode, // statusCode
107-
resp.Status, // status
108-
fmt.Errorf("Non-JSON error response received"), // err
109-
apiError.Raw, // raw resp
123+
log.LogError("xml_parsing_error",
124+
resp.Request.Method,
125+
resp.Request.URL.String(),
126+
apiError.StatusCode,
127+
fmt.Sprintf("Failed to parse XML: %s", err),
128+
err,
129+
apiError.Raw,
110130
)
111-
return apiError
112-
}
131+
} else {
132+
// Update the APIError with information from the parsed XML
133+
apiError.Message = xmlErr.Message
134+
// Assuming you might want to add a 'Code' field to APIError to store xmlErr.Code
135+
// apiError.Code = xmlErr.Code
113136

114-
return apiError
137+
// Log the parsed error details
138+
log.LogError("xml_error_detected",
139+
resp.Request.Method,
140+
resp.Request.URL.String(),
141+
apiError.StatusCode,
142+
"Parsed XML error successfully",
143+
nil, // No error during parsing
144+
apiError.Raw,
145+
)
146+
}
115147
}
116148

117-
// isJSONResponse checks if the response Content-Type indicates JSON
118-
func isJSONResponse(resp *http.Response) bool {
119-
contentType := resp.Header.Get("Content-Type")
120-
return strings.Contains(contentType, "application/json")
149+
// parseTextResponse updates the APIError structure based on a plain text error response and logs it.
150+
func parseTextResponse(bodyBytes []byte, apiError *APIError, log logger.Logger, resp *http.Response) {
151+
bodyText := string(bodyBytes)
152+
apiError.Message = bodyText
153+
apiError.Raw = bodyText
154+
// Log the plain text error using the centralized logger.
155+
logError(log, apiError, "text_error_detected", resp)
121156
}
122157

123-
// isHTMLResponse checks if the response Content-Type indicates HTML
124-
func isHTMLResponse(resp *http.Response) bool {
125-
contentType := resp.Header.Get("Content-Type")
126-
return strings.Contains(contentType, "text/html")
127-
}
158+
// parseHTMLResponse extracts meaningful information from an HTML error response.
159+
func parseHTMLResponse(bodyBytes []byte, apiError *APIError, log logger.Logger, resp *http.Response) {
160+
// Convert the response body to a reader for the HTML parser
161+
reader := bytes.NewReader(bodyBytes)
162+
doc, err := html.Parse(reader)
163+
if err != nil {
164+
apiError.Raw = string(bodyBytes)
165+
logError(log, apiError, "html_parsing_error", resp)
166+
return
167+
}
128168

129-
// updateFromGenericError updates the APIError fields based on a generic error map extracted from an API response.
130-
// This function is useful for cases where the error response does not match the predefined StructuredError format,
131-
// and instead, a more generic error handling approach is needed. It extracts known fields such as 'message' and 'detail'
132-
// from the generic error map and updates the corresponding fields in the APIError instance.
133-
//
134-
// Parameters:
135-
// - genericErr: A map[string]interface{} representing the generic error structure extracted from an API response.
136-
//
137-
// The function checks for the presence of 'message' and 'detail' keys in the generic error map. If these keys are present,
138-
// their values are used to update the 'Message' and 'Detail' fields of the APIError instance, respectively.
139-
func (e *APIError) updateFromGenericError(genericErr map[string]interface{}) {
140-
if msg, ok := genericErr["message"].(string); ok {
141-
e.Message = msg
169+
var parse func(*html.Node)
170+
parse = func(n *html.Node) {
171+
// Look for <p> tags that might contain error messages
172+
if n.Type == html.ElementNode && n.Data == "p" {
173+
if n.FirstChild != nil {
174+
// Assuming the error message is in the text content of a <p> tag
175+
apiError.Message = n.FirstChild.Data
176+
return // Stop after finding the first <p> tag with content
177+
}
178+
}
179+
for c := n.FirstChild; c != nil; c = c.NextSibling {
180+
parse(c) // Recursively search for <p> tags in child nodes
181+
}
142182
}
143-
if detail, ok := genericErr["detail"].(string); ok {
144-
e.Detail = detail
183+
184+
parse(doc) // Start parsing from the document node
185+
186+
// If no <p> tag was found or it was empty, fallback to using the raw HTML
187+
if apiError.Message == "" {
188+
apiError.Message = "HTML Error: See 'Raw' field for details."
189+
apiError.Raw = string(bodyBytes)
145190
}
146-
// Optionally add more fields if necessary
191+
// Log the extracted error message or the fallback message
192+
logError(log, apiError, "html_error_detected", resp)
193+
}
194+
195+
// logError logs the error details using the provided logger instance.
196+
func logError(log logger.Logger, apiError *APIError, event string, resp *http.Response) {
197+
// Prepare the error message. If apiError.Message is empty, use a default message.
198+
errorMessage := apiError.Message
199+
if errorMessage == "" {
200+
errorMessage = "An unspecified error occurred"
201+
}
202+
203+
// Use LogError method from the logger package for error logging.
204+
log.LogError(
205+
event,
206+
resp.Request.Method,
207+
resp.Request.URL.String(),
208+
apiError.StatusCode,
209+
resp.Status,
210+
fmt.Errorf(errorMessage),
211+
apiError.Raw,
212+
)
147213
}

0 commit comments

Comments
 (0)