@@ -6,6 +6,7 @@ package response
6
6
import (
7
7
"encoding/json"
8
8
"encoding/xml"
9
+ "errors"
9
10
"fmt"
10
11
"io"
11
12
"net/http"
@@ -15,32 +16,39 @@ import (
15
16
"go.uber.org/zap"
16
17
)
17
18
18
- // HandleAPISuccessResponse handles the HTTP success response from an API and unmarshals the response body into the provided output struct.
19
+ // Refactored contentHandler to accept io.Reader instead of []byte for streaming support.
20
+ type contentHandler func (io.Reader , interface {}, logger.Logger , string ) error
21
+
22
+ // Updated handlers map to use the new contentHandler signature.
23
+ var handlers = map [string ]contentHandler {
24
+ "application/json" : unmarshalJSON ,
25
+ "application/xml" : unmarshalXML ,
26
+ "text/xml" : unmarshalXML ,
27
+ }
28
+
29
+ // HandleAPISuccessResponse reads the response body and unmarshals it based on the content type.
19
30
func HandleAPISuccessResponse (resp * http.Response , out interface {}, log logger.Logger ) error {
20
- // Special handling for DELETE requests
21
31
if resp .Request .Method == "DELETE" {
22
32
return handleDeleteRequest (resp , log )
23
33
}
24
34
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" )
35
+ // No need to read the entire body into memory, pass resp.Body directly.
36
+ logResponseDetails (resp , nil , log ) // Updated to handle nil bodyBytes.
36
37
37
- // Check for binary data handling
38
+ mimeType , _ := ParseContentTypeHeader ( resp . Header . Get ( "Content-Type" ))
38
39
contentDisposition := resp .Header .Get ("Content-Disposition" )
39
- if err := handleBinaryData (contentType , contentDisposition , bodyBytes , log , out ); err != nil {
40
- return err
41
- }
42
40
43
- return unmarshalResponse (contentType , bodyBytes , log , out )
41
+ if handler , ok := handlers [mimeType ]; ok {
42
+ // Pass resp.Body directly to the handler for streaming.
43
+ return handler (resp .Body , out , log , mimeType )
44
+ } else if isBinaryData (mimeType , contentDisposition ) {
45
+ // For binary data, we still need to handle the body directly.
46
+ return handleBinaryData (resp .Body , log , out , mimeType , contentDisposition )
47
+ } else {
48
+ errMsg := fmt .Sprintf ("unexpected MIME type: %s" , mimeType )
49
+ log .Error ("Unmarshal error" , zap .String ("content type" , mimeType ), zap .Error (errors .New (errMsg )))
50
+ return errors .New (errMsg )
51
+ }
44
52
}
45
53
46
54
// handleDeleteRequest handles the special case for DELETE requests, where a successful response might not contain a body.
@@ -52,79 +60,78 @@ func handleDeleteRequest(resp *http.Response, log logger.Logger) error {
52
60
return log .Error ("DELETE request failed" , zap .String ("URL" , resp .Request .URL .String ()), zap .Int ("Status Code" , resp .StatusCode ))
53
61
}
54
62
55
- // readResponseBody reads and returns the body of an HTTP response. It logs an error if reading fails.
56
- func readResponseBody (resp * http.Response , log logger.Logger ) ([]byte , error ) {
57
- // Read the response body
58
- bodyBytes , err := io .ReadAll (resp .Body )
59
- if err != nil {
60
- log .Error ("Failed reading response body" , zap .Error (err ))
61
- return nil , err
63
+ // Adjusted logResponseDetails to handle a potential nil bodyBytes.
64
+ func logResponseDetails (resp * http.Response , bodyBytes []byte , log logger.Logger ) {
65
+ // Conditional logging if bodyBytes is not nil.
66
+ if bodyBytes != nil {
67
+ log .Debug ("Raw HTTP Response" , zap .String ("Body" , string (bodyBytes )))
62
68
}
63
- return bodyBytes , nil
69
+ // Logging headers remains unchanged.
70
+ log .Debug ("HTTP Response Headers" , zap .Any ("Headers" , resp .Header ))
64
71
}
65
72
66
- // logResponseDetails logs the raw HTTP response body and headers for debugging purposes.
67
- func logResponseDetails (resp * http.Response , bodyBytes []byte , log logger.Logger ) {
68
- // Log the response body as a string
69
- log .Debug ("Raw HTTP Response" , zap .String ("Body" , string (bodyBytes )))
70
- // Log the response headers
71
- log .Debug ("HTTP Response Headers" , zap .Any ("Headers" , resp .Header ))
73
+ // unmarshalJSON unmarshals JSON content from an io.Reader into the provided output structure.
74
+ func unmarshalJSON (reader io.Reader , out interface {}, log logger.Logger , mimeType string ) error {
75
+ decoder := json .NewDecoder (reader )
76
+ if err := decoder .Decode (out ); err != nil {
77
+ log .Error ("JSON Unmarshal error" , zap .Error (err ))
78
+ return err
79
+ }
80
+ log .Info ("Successfully unmarshalled JSON response" , zap .String ("content type" , mimeType ))
81
+ return nil
72
82
}
73
83
74
- // 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.
75
- func handleBinaryData (contentType , contentDisposition string , bodyBytes []byte , log logger.Logger , out interface {}) error {
76
- // Check if response is binary data either by Content-Type or Content-Disposition
77
- if strings .Contains (contentType , "application/octet-stream" ) || strings .HasPrefix (contentDisposition , "attachment" ) {
78
- // Assert that 'out' is of the correct type to receive binary data
79
- if outPointer , ok := out .(* []byte ); ok {
80
- * outPointer = bodyBytes // Assign the response body to 'out'
81
- log .Debug ("Handled binary data" , // Log handling of binary data
82
- zap .String ("Content-Type" , contentType ),
83
- zap .String ("Content-Disposition" , contentDisposition ),
84
- )
85
- return nil
86
- } else {
87
- errMsg := "output parameter is not a *[]byte for binary data"
88
- log .Error ("Binary data handling error" , // Log error for incorrect 'out' type
89
- zap .String ("error" , errMsg ),
90
- zap .String ("Content-Type" , contentType ),
91
- zap .String ("Content-Disposition" , contentDisposition ),
92
- )
93
- return fmt .Errorf (errMsg )
94
- }
84
+ // unmarshalXML unmarshals XML content from an io.Reader into the provided output structure.
85
+ func unmarshalXML (reader io.Reader , out interface {}, log logger.Logger , mimeType string ) error {
86
+ decoder := xml .NewDecoder (reader )
87
+ if err := decoder .Decode (out ); err != nil {
88
+ log .Error ("XML Unmarshal error" , zap .Error (err ))
89
+ return err
95
90
}
96
- return nil // If not binary data, no action needed
91
+ log .Info ("Successfully unmarshalled XML response" , zap .String ("content type" , mimeType ))
92
+ return nil
97
93
}
98
94
99
- // unmarshalResponse unmarshals the response body into the provided output structure based on the MIME
100
- // type extracted from the Content-Type header.
101
- func unmarshalResponse (contentTypeHeader string , bodyBytes []byte , log logger.Logger , out interface {}) error {
102
- // Extract MIME type from Content-Type header
103
- mimeType , _ := ParseContentTypeHeader (contentTypeHeader )
104
-
105
- // Determine the MIME type and unmarshal accordingly
106
- switch {
107
- case strings .Contains (mimeType , "application/json" ):
108
- // Unmarshal JSON content
109
- if err := json .Unmarshal (bodyBytes , out ); err != nil {
110
- log .Error ("JSON Unmarshal error" , zap .Error (err ))
95
+ // isBinaryData checks if the MIME type or Content-Disposition indicates binary data.
96
+ func isBinaryData (contentType , contentDisposition string ) bool {
97
+ return strings .Contains (contentType , "application/octet-stream" ) || strings .HasPrefix (contentDisposition , "attachment" )
98
+ }
99
+
100
+ // handleBinaryData reads binary data from an io.Reader and stores it in *[]byte or streams it to an io.Writer.
101
+ func handleBinaryData (reader io.Reader , log logger.Logger , out interface {}, mimeType , contentDisposition string ) error {
102
+ // Check if the output interface is either *[]byte or io.Writer
103
+ switch out := out .(type ) {
104
+ case * []byte :
105
+ // Read all data from reader and store it in *[]byte
106
+ data , err := io .ReadAll (reader )
107
+ if err != nil {
108
+ log .Error ("Failed to read binary data" , zap .Error (err ))
111
109
return err
112
110
}
113
- log . Info ( "Successfully unmarshalled JSON response" , zap . String ( "content type" , mimeType ))
111
+ * out = data
114
112
115
- case strings .Contains (mimeType , "application/xml" ) || strings .Contains (mimeType , "text/xml" ):
116
- // Unmarshal XML content
117
- if err := xml .Unmarshal (bodyBytes , out ); err != nil {
118
- log .Error ("XML Unmarshal error" , zap .Error (err ))
113
+ case io.Writer :
114
+ // Stream data directly to the io.Writer
115
+ _ , err := io .Copy (out , reader )
116
+ if err != nil {
117
+ log .Error ("Failed to stream binary data to io.Writer" , zap .Error (err ))
119
118
return err
120
119
}
121
- log .Info ("Successfully unmarshalled XML response" , zap .String ("content type" , mimeType ))
122
120
123
121
default :
124
- // Log and return an error for unexpected MIME types
125
- errMsg := fmt .Sprintf ("unexpected MIME type: %s" , mimeType )
126
- log .Error ("Unmarshal error" , zap .String ("content type" , mimeType ), zap .Error (fmt .Errorf (errMsg )))
127
- return fmt .Errorf (errMsg )
122
+ errMsg := "output parameter is not suitable for binary data (*[]byte or io.Writer)"
123
+ log .Error (errMsg , zap .String ("Content-Type" , mimeType ))
124
+ return errors .New (errMsg )
128
125
}
126
+
127
+ // Handle Content-Disposition if present
128
+ if contentDisposition != "" {
129
+ _ , params := ParseContentDisposition (contentDisposition )
130
+ if filename , ok := params ["filename" ]; ok {
131
+ log .Debug ("Extracted filename from Content-Disposition" , zap .String ("filename" , filename ))
132
+ // Additional processing for the filename can be done here if needed
133
+ }
134
+ }
135
+
129
136
return nil
130
137
}
0 commit comments