|
3 | 3 | package response
|
4 | 4 |
|
5 | 5 | import (
|
| 6 | + "bytes" |
6 | 7 | "encoding/json"
|
| 8 | + "encoding/xml" |
7 | 9 | "fmt"
|
8 | 10 | "io"
|
9 | 11 | "net/http"
|
10 | 12 | "strings"
|
11 | 13 |
|
12 | 14 | "github.com/deploymenttheory/go-api-http-client/logger"
|
| 15 | + "golang.org/x/net/html" |
13 | 16 | )
|
14 | 17 |
|
15 |
| -// APIError represents a more flexible structure for API error responses. |
| 18 | +// APIError represents an api error response. |
16 | 19 | 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 |
23 | 26 | }
|
24 | 27 |
|
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. |
34 | 29 | func (e *APIError) Error() string {
|
| 30 | + // Attempt to marshal the APIError instance into a JSON string. |
35 | 31 | 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) |
38 | 39 | }
|
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) |
40 | 43 | }
|
41 | 44 |
|
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. |
43 | 46 | func HandleAPIErrorResponse(resp *http.Response, log logger.Logger) *APIError {
|
44 | 47 | apiError := &APIError{
|
45 | 48 | 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", |
48 | 51 | }
|
49 | 52 |
|
50 | 53 | bodyBytes, err := io.ReadAll(resp.Body)
|
51 | 54 | if err != nil {
|
| 55 | + apiError.Raw = "Failed to read response body" |
| 56 | + logError(log, apiError, "error_reading_response_body", resp) |
52 | 57 | return apiError
|
53 | 58 | }
|
54 | 59 |
|
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 | +} |
70 | 82 |
|
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]) |
85 | 92 | }
|
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 { |
88 | 100 | 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, |
97 | 108 | )
|
98 |
| - return apiError |
99 | 109 | } 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 |
101 | 122 | 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, |
110 | 130 | )
|
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 |
113 | 136 |
|
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 | + } |
115 | 147 | }
|
116 | 148 |
|
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) |
121 | 156 | }
|
122 | 157 |
|
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 | + } |
128 | 168 |
|
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 | + } |
142 | 182 | }
|
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) |
145 | 190 | }
|
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 | + ) |
147 | 213 | }
|
0 commit comments