Skip to content

Commit 128fbf6

Browse files
committed
Refactor success response handling
1 parent 78471dc commit 128fbf6

File tree

8 files changed

+169
-102
lines changed

8 files changed

+169
-102
lines changed

apiintegrations/apihandler/apihandler.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
package apihandler
33

44
import (
5-
"net/http"
6-
75
"github.com/deploymenttheory/go-api-http-client/apiintegrations/jamfpro"
86
"github.com/deploymenttheory/go-api-http-client/apiintegrations/msgraph"
97
"github.com/deploymenttheory/go-api-http-client/logger"
@@ -17,7 +15,7 @@ type APIHandler interface {
1715
ConstructAPIAuthEndpoint(instanceName string, endpointPath string, log logger.Logger) string
1816
MarshalRequest(body interface{}, method string, endpoint string, log logger.Logger) ([]byte, error)
1917
MarshalMultipartRequest(fields map[string]string, files map[string]string, log logger.Logger) ([]byte, string, error)
20-
HandleAPISuccessResponse(resp *http.Response, out interface{}, log logger.Logger) error
18+
//HandleAPISuccessResponse(resp *http.Response, out interface{}, log logger.Logger) error
2119
GetContentTypeHeader(method string, log logger.Logger) string
2220
GetAcceptHeader() string
2321
GetDefaultBaseDomain() string
File renamed without changes.

httpclient/multipartrequest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,6 @@ func (c *Client) DoMultipartRequest(method, endpoint string, fields map[string]s
9090
return nil, response.HandleAPIErrorResponse(resp, log)
9191
} else {
9292
// Handle successful responses
93-
return resp, c.handleSuccessResponse(resp, out, log, method, endpoint)
93+
return resp, response.HandleAPISuccessResponse(resp, out, log)
9494
}
9595
}

httpclient/request.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ func (c *Client) executeRequestWithRetries(method, endpoint string, body, out in
186186
if resp.StatusCode >= 300 {
187187
log.Warn("Redirect response received", zap.Int("status_code", resp.StatusCode), zap.String("location", resp.Header.Get("Location")))
188188
}
189-
// Handle the response as successful, even if it's a redirect.
190-
return resp, c.handleSuccessResponse(resp, out, log, method, endpoint)
189+
// Handle the response as successful.
190+
return resp, response.HandleAPISuccessResponse(resp, out, log)
191191
}
192192

193193
// Leverage TranslateStatusCode for more descriptive error logging
@@ -348,11 +348,11 @@ func (c *Client) executeRequest(method, endpoint string, body, out interface{})
348348
if resp.StatusCode >= 300 {
349349
log.Warn("Redirect response received", zap.Int("status_code", resp.StatusCode), zap.String("location", resp.Header.Get("Location")))
350350
}
351-
return resp, c.handleSuccessResponse(resp, out, log, method, endpoint)
351+
return resp, response.HandleAPISuccessResponse(resp, out, log)
352+
352353
}
353354

354355
// Handle error responses for status codes outside the successful range
355-
//return nil, c.handleErrorResponse(resp, out, log, method, endpoint)
356356
return nil, response.HandleAPIErrorResponse(resp, log)
357357
}
358358

@@ -430,19 +430,19 @@ func (c *Client) do(req *http.Request, log logger.Logger, method, endpoint strin
430430
//
431431
// Returns:
432432
// - nil if the response was successfully unmarshalled into the 'out' parameter, or an error if unmarshalling failed.
433-
func (c *Client) handleSuccessResponse(resp *http.Response, out interface{}, log logger.Logger, method, endpoint string) error {
434-
if err := c.APIHandler.HandleAPISuccessResponse(resp, out, log); err != nil {
435-
log.Error("Failed to unmarshal HTTP response",
436-
zap.String("method", method),
437-
zap.String("endpoint", endpoint),
438-
zap.Error(err),
439-
)
440-
return err
441-
}
442-
log.Info("HTTP request succeeded",
443-
zap.String("method", method),
444-
zap.String("endpoint", endpoint),
445-
zap.Int("status_code", resp.StatusCode),
446-
)
447-
return nil
448-
}
433+
// func (c *Client) handleSuccessResponse(resp *http.Response, out interface{}, log logger.Logger, method, endpoint string) error {
434+
// if err := c.APIHandler.HandleAPISuccessResponse(resp, out, log); err != nil {
435+
// log.Error("Failed to unmarshal HTTP response",
436+
// zap.String("method", method),
437+
// zap.String("endpoint", endpoint),
438+
// zap.Error(err),
439+
// )
440+
// return err
441+
// }
442+
// log.Info("HTTP request succeeded",
443+
// zap.String("method", method),
444+
// zap.String("endpoint", endpoint),
445+
// zap.Int("status_code", resp.StatusCode),
446+
// )
447+
// return nil
448+
// }

response/error.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,6 @@ func HandleAPIErrorResponse(resp *http.Response, log logger.Logger) *APIError {
7777
return apiError
7878
}
7979

80-
// ParseContentTypeHeader parses the Content-Type header and extracts the MIME type and parameters.
81-
func ParseContentTypeHeader(header string) (string, map[string]string) {
82-
parts := strings.Split(header, ";")
83-
mimeType := strings.TrimSpace(parts[0])
84-
params := make(map[string]string)
85-
for _, part := range parts[1:] {
86-
kv := strings.SplitN(part, "=", 2)
87-
if len(kv) == 2 {
88-
params[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
89-
}
90-
}
91-
return mimeType, params
92-
}
93-
9480
// parseJSONResponse attempts to parse the JSON error response and update the APIError structure.
9581
func parseJSONResponse(bodyBytes []byte, apiError *APIError, log logger.Logger, resp *http.Response) {
9682
if err := json.Unmarshal(bodyBytes, apiError); err != nil {

response/error_test.go.TODO

Lines changed: 0 additions & 64 deletions
This file was deleted.

response/parse.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// response/parse.go
2+
package response
3+
4+
import "strings"
5+
6+
// ParseContentTypeHeader parses the Content-Type header and extracts the MIME type and parameters.
7+
func ParseContentTypeHeader(header string) (string, map[string]string) {
8+
parts := strings.Split(header, ";")
9+
mimeType := strings.TrimSpace(parts[0])
10+
params := make(map[string]string)
11+
for _, part := range parts[1:] {
12+
kv := strings.SplitN(part, "=", 2)
13+
if len(kv) == 2 {
14+
params[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
15+
}
16+
}
17+
return mimeType, params
18+
}

response/success.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// response/success.go
2+
/* Responsible for handling successful API responses. It reads the response body, logs the raw response details,
3+
and unmarshals the response based on the content type (JSON or XML). */
4+
package response
5+
6+
import (
7+
"encoding/json"
8+
"encoding/xml"
9+
"fmt"
10+
"io"
11+
"net/http"
12+
"strings"
13+
14+
"github.com/deploymenttheory/go-api-http-client/logger"
15+
"go.uber.org/zap"
16+
)
17+
18+
// HandleAPISuccessResponse handles the HTTP success response from an API and unmarshals the response body into the provided output struct.
19+
func HandleAPISuccessResponse(resp *http.Response, out interface{}, log logger.Logger) error {
20+
// Special handling for DELETE requests
21+
if resp.Request.Method == "DELETE" {
22+
return handleDeleteRequest(resp, log)
23+
}
24+
25+
// Read the response body
26+
bodyBytes, err := readResponseBody(resp, log)
27+
if err != nil {
28+
return err
29+
}
30+
31+
// Log the raw response details for debugging
32+
logResponseDetails(resp, bodyBytes, log)
33+
34+
// Unmarshal the response based on content type
35+
contentType := resp.Header.Get("Content-Type")
36+
37+
// Check for binary data handling
38+
contentDisposition := resp.Header.Get("Content-Disposition")
39+
if err := handleBinaryData(contentType, contentDisposition, bodyBytes, log, out); err != nil {
40+
return err
41+
}
42+
43+
return unmarshalResponse(contentType, bodyBytes, log, out)
44+
}
45+
46+
// handleDeleteRequest handles the special case for DELETE requests, where a successful response might not contain a body.
47+
func handleDeleteRequest(resp *http.Response, log logger.Logger) error {
48+
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
49+
return nil
50+
}
51+
return log.Error("DELETE request failed", zap.Int("Status Code", resp.StatusCode))
52+
}
53+
54+
// readResponseBody reads and returns the body of an HTTP response. It logs an error if reading fails.
55+
func readResponseBody(resp *http.Response, log logger.Logger) ([]byte, error) {
56+
// Read the response body
57+
bodyBytes, err := io.ReadAll(resp.Body)
58+
if err != nil {
59+
log.Error("Failed reading response body", zap.Error(err))
60+
return nil, err
61+
}
62+
return bodyBytes, nil
63+
}
64+
65+
// logResponseDetails logs the raw HTTP response body and headers for debugging purposes.
66+
func logResponseDetails(resp *http.Response, bodyBytes []byte, log logger.Logger) {
67+
// Log the response body as a string
68+
log.Debug("Raw HTTP Response", zap.String("Body", string(bodyBytes)))
69+
// Log the response headers
70+
log.Debug("HTTP Response Headers", zap.Any("Headers", resp.Header))
71+
}
72+
73+
// handleBinaryData checks if the response should be treated as binary data based on the Content-Type or Content-Disposition headers. It assigns the response body to 'out' if 'out' is of type *[]byte.
74+
func handleBinaryData(contentType, contentDisposition string, bodyBytes []byte, log logger.Logger, out interface{}) error {
75+
// Check if response is binary data either by Content-Type or Content-Disposition
76+
if strings.Contains(contentType, "application/octet-stream") || strings.HasPrefix(contentDisposition, "attachment") {
77+
// Assert that 'out' is of the correct type to receive binary data
78+
if outPointer, ok := out.(*[]byte); ok {
79+
*outPointer = bodyBytes // Assign the response body to 'out'
80+
log.Debug("Handled binary data", // Log handling of binary data
81+
zap.String("Content-Type", contentType),
82+
zap.String("Content-Disposition", contentDisposition),
83+
)
84+
return nil
85+
} else {
86+
errMsg := "output parameter is not a *[]byte for binary data"
87+
log.Error("Binary data handling error", // Log error for incorrect 'out' type
88+
zap.String("error", errMsg),
89+
zap.String("Content-Type", contentType),
90+
zap.String("Content-Disposition", contentDisposition),
91+
)
92+
return fmt.Errorf(errMsg)
93+
}
94+
}
95+
return nil // If not binary data, no action needed
96+
}
97+
98+
// unmarshalResponse unmarshals the response body into the provided output structure based on the MIME
99+
// type extracted from the Content-Type header.
100+
func unmarshalResponse(contentTypeHeader string, bodyBytes []byte, log logger.Logger, out interface{}) error {
101+
// Extract MIME type from Content-Type header
102+
mimeType, _ := ParseContentTypeHeader(contentTypeHeader)
103+
104+
// Determine the MIME type and unmarshal accordingly
105+
switch {
106+
case strings.Contains(mimeType, "application/json"):
107+
// Unmarshal JSON content
108+
if err := json.Unmarshal(bodyBytes, out); err != nil {
109+
log.Error("JSON Unmarshal error", zap.Error(err))
110+
return err
111+
}
112+
log.Info("Successfully unmarshalled JSON response", zap.String("content type", mimeType))
113+
114+
case strings.Contains(mimeType, "application/xml") || strings.Contains(mimeType, "text/xml"):
115+
// Unmarshal XML content
116+
if err := xml.Unmarshal(bodyBytes, out); err != nil {
117+
log.Error("XML Unmarshal error", zap.Error(err))
118+
return err
119+
}
120+
log.Info("Successfully unmarshalled XML response", zap.String("content type", mimeType))
121+
122+
default:
123+
// Log and return an error for unexpected MIME types
124+
errMsg := fmt.Sprintf("unexpected MIME type: %s", mimeType)
125+
log.Error("Unmarshal error", zap.String("content type", mimeType), zap.Error(fmt.Errorf(errMsg)))
126+
return fmt.Errorf(errMsg)
127+
}
128+
return nil
129+
}

0 commit comments

Comments
 (0)