Skip to content

Commit 75accdf

Browse files
authored
Merge pull request #54 from deploymenttheory/dev
Dev
2 parents e367a50 + 12dbde0 commit 75accdf

File tree

9 files changed

+582
-242
lines changed

9 files changed

+582
-242
lines changed

apihandlers/jamfpro/jamfpro_api_error_messages.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,72 @@ package jamfpro
33
import (
44
"bytes"
55
"encoding/json"
6+
"io"
7+
"net/http"
68
"strings"
79

810
"github.com/PuerkitoBio/goquery"
911
)
1012

13+
// APIHandlerError represents an error response from the Jamf Pro API.
14+
type APIHandlerError struct {
15+
HTTPStatusCode int `json:"httpStatusCode"`
16+
ErrorType string `json:"errorType"`
17+
ErrorMessage string `json:"errorMessage"`
18+
ExtraDetails map[string]interface{} `json:"extraDetails"`
19+
}
20+
21+
// ReturnAPIErrorResponse parses an HTTP error response from the Jamf Pro API.
22+
func (j *JamfAPIHandler) ReturnAPIErrorResponse(resp *http.Response) *APIHandlerError {
23+
var errorMessage, errorType string
24+
var extraDetails map[string]interface{}
25+
26+
// Safely read the response body
27+
bodyBytes, readErr := io.ReadAll(resp.Body)
28+
if readErr != nil {
29+
return &APIHandlerError{
30+
HTTPStatusCode: resp.StatusCode,
31+
ErrorType: "ReadError",
32+
ErrorMessage: "Failed to read response body",
33+
}
34+
}
35+
36+
// Ensure the body can be re-read for subsequent operations
37+
resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
38+
39+
contentType := resp.Header.Get("Content-Type")
40+
41+
// Handle JSON content type
42+
if strings.Contains(contentType, "application/json") {
43+
description, parseErr := ParseJSONErrorResponse(bodyBytes)
44+
if parseErr == nil {
45+
errorMessage = description
46+
errorType = "JSONError"
47+
} else {
48+
errorMessage = "Failed to parse JSON error response: " + parseErr.Error()
49+
}
50+
} else if strings.Contains(contentType, "text/html") {
51+
// Handle HTML content type
52+
bodyBytes, err := io.ReadAll(resp.Body)
53+
if err == nil {
54+
errorMessage = ExtractErrorMessageFromHTML(string(bodyBytes))
55+
errorType = "HTMLError"
56+
} else {
57+
errorMessage = "Failed to read response body for HTML error parsing"
58+
}
59+
} else {
60+
// Fallback for unhandled content types
61+
errorMessage = "An unknown error occurred"
62+
}
63+
64+
return &APIHandlerError{
65+
HTTPStatusCode: resp.StatusCode,
66+
ErrorType: errorType,
67+
ErrorMessage: errorMessage,
68+
ExtraDetails: extraDetails,
69+
}
70+
}
71+
1172
// ExtractErrorMessageFromHTML attempts to parse an HTML error page and extract a combined human-readable error message.
1273
func ExtractErrorMessageFromHTML(htmlContent string) string {
1374
r := bytes.NewReader([]byte(htmlContent))

apihandlers/jamfpro/jamfpro_api_exceptions.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import (
77
"log"
88
)
99

10+
// EndpointConfig is a struct that holds configuration details for a specific API endpoint.
11+
// It includes what type of content it can accept and what content type it should send.
12+
type EndpointConfig struct {
13+
Accept string `json:"accept"` // Accept specifies the MIME type the endpoint can handle in responses.
14+
ContentType *string `json:"content_type"` // ContentType, if not nil, specifies the MIME type to set for requests sent to the endpoint. A pointer is used to distinguish between a missing field and an empty string.
15+
}
16+
1017
// ConfigMap is a map that associates endpoint URL patterns with their corresponding configurations.
1118
// The map's keys are strings that identify the endpoint, and the values are EndpointConfig structs
1219
// that hold the configuration for that endpoint.
Lines changed: 1 addition & 236 deletions
Original file line numberDiff line numberDiff line change
@@ -1,245 +1,10 @@
1-
// jamfpro_api_handler.go
21
package jamfpro
32

4-
import (
5-
"bytes"
6-
"encoding/json"
7-
"encoding/xml"
8-
"fmt"
9-
"io"
10-
"mime/multipart"
11-
"net/http"
12-
"os"
13-
"strings"
14-
15-
"github.com/deploymenttheory/go-api-http-client/logger"
16-
"go.uber.org/zap"
17-
)
18-
19-
// EndpointConfig is a struct that holds configuration details for a specific API endpoint.
20-
// It includes what type of content it can accept and what content type it should send.
21-
type EndpointConfig struct {
22-
Accept string `json:"accept"` // Accept specifies the MIME type the endpoint can handle in responses.
23-
ContentType *string `json:"content_type"` // ContentType, if not nil, specifies the MIME type to set for requests sent to the endpoint. A pointer is used to distinguish between a missing field and an empty string.
24-
}
3+
import "github.com/deploymenttheory/go-api-http-client/logger"
254

265
// JamfAPIHandler implements the APIHandler interface for the Jamf Pro API.
276
type JamfAPIHandler struct {
287
OverrideBaseDomain string // OverrideBaseDomain is used to override the base domain for URL construction.
298
InstanceName string // InstanceName is the name of the Jamf instance.
309
Logger logger.Logger // Logger is the structured logger used for logging.
3110
}
32-
33-
// Functions
34-
35-
// MarshalRequest encodes the request body according to the endpoint for the API.
36-
func (j *JamfAPIHandler) MarshalRequest(body interface{}, method string, endpoint string, log logger.Logger) ([]byte, error) {
37-
var (
38-
data []byte
39-
err error
40-
)
41-
42-
// Determine the format based on the endpoint
43-
format := "json"
44-
if strings.Contains(endpoint, "/JSSResource") {
45-
format = "xml"
46-
} else if strings.Contains(endpoint, "/api") {
47-
format = "json"
48-
}
49-
50-
switch format {
51-
case "xml":
52-
data, err = xml.Marshal(body)
53-
if err != nil {
54-
return nil, err
55-
}
56-
57-
if method == "POST" || method == "PUT" {
58-
j.Logger.Debug("XML Request Body", zap.String("Body", string(data)))
59-
}
60-
61-
case "json":
62-
data, err = json.Marshal(body)
63-
if err != nil {
64-
j.Logger.Error("Failed marshaling JSON request", zap.Error(err))
65-
return nil, err
66-
}
67-
68-
if method == "POST" || method == "PUT" || method == "PATCH" {
69-
j.Logger.Debug("JSON Request Body", zap.String("Body", string(data)))
70-
}
71-
}
72-
73-
return data, nil
74-
}
75-
76-
// UnmarshalResponse decodes the response body from XML or JSON format depending on the Content-Type header.
77-
func (j *JamfAPIHandler) UnmarshalResponse(resp *http.Response, out interface{}, log logger.Logger) error {
78-
// Handle DELETE method
79-
if resp.Request.Method == "DELETE" {
80-
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
81-
return nil
82-
} else {
83-
return j.Logger.Error("DELETE request failed", zap.Int("Status Code", resp.StatusCode))
84-
}
85-
}
86-
87-
bodyBytes, err := io.ReadAll(resp.Body)
88-
if err != nil {
89-
j.Logger.Error("Failed reading response body", zap.Error(err))
90-
return err
91-
}
92-
93-
// Log the raw response body and headers
94-
j.Logger.Debug("Raw HTTP Response", zap.String("Body", string(bodyBytes)))
95-
j.Logger.Debug("Unmarshaling response", zap.String("status", resp.Status))
96-
97-
// Log headers when in debug mode
98-
j.Logger.Debug("HTTP Response Headers", zap.Any("Headers", resp.Header))
99-
100-
// Check the Content-Type and Content-Disposition headers
101-
contentType := resp.Header.Get("Content-Type")
102-
contentDisposition := resp.Header.Get("Content-Disposition")
103-
104-
// Handle binary data if necessary
105-
if err := j.handleBinaryData(contentType, contentDisposition, bodyBytes, out); err != nil {
106-
return err
107-
}
108-
109-
// Check for non-success status codes
110-
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
111-
// If the content type is HTML, extract and log the error message
112-
if strings.Contains(contentType, "text/html") {
113-
htmlErrorMessage := ExtractErrorMessageFromHTML(string(bodyBytes))
114-
115-
// Log the HTML error message using Zap
116-
j.Logger.Error("Received HTML error content",
117-
zap.String("error_message", htmlErrorMessage),
118-
zap.Int("status_code", resp.StatusCode),
119-
)
120-
} else {
121-
// Log a generic error message if the response is not HTML
122-
j.Logger.Error("Received non-success status code without detailed error response",
123-
zap.Int("status_code", resp.StatusCode),
124-
)
125-
}
126-
}
127-
128-
// Check for non-success status codes before attempting to unmarshal
129-
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
130-
// Parse the error details from the response body for JSON content type
131-
if strings.Contains(contentType, "application/json") {
132-
description, err := ParseJSONErrorResponse(bodyBytes)
133-
if err != nil {
134-
// Log the error using the structured logger and return the error
135-
j.Logger.Error("Failed to parse JSON error response",
136-
zap.Error(err),
137-
zap.Int("status_code", resp.StatusCode),
138-
)
139-
return err
140-
}
141-
// Log the error with description using the structured logger and return the error
142-
j.Logger.Error("Received non-success status code with JSON response",
143-
zap.Int("status_code", resp.StatusCode),
144-
zap.String("error_description", description),
145-
)
146-
return fmt.Errorf("received non-success status code with JSON response: %s", description)
147-
}
148-
149-
// If the response is not JSON or another error occurs, log a generic error message and return an error
150-
j.Logger.Error("Received non-success status code without JSON response",
151-
zap.Int("status_code", resp.StatusCode),
152-
)
153-
return fmt.Errorf("received non-success status code without JSON response: %d", resp.StatusCode)
154-
}
155-
156-
// Determine whether the content type is JSON or XML and unmarshal accordingly
157-
switch {
158-
case strings.Contains(contentType, "application/json"):
159-
err = json.Unmarshal(bodyBytes, out)
160-
case strings.Contains(contentType, "application/xml"), strings.Contains(contentType, "text/xml;charset=UTF-8"):
161-
err = xml.Unmarshal(bodyBytes, out)
162-
default:
163-
// If the content type is neither JSON nor XML nor HTML
164-
return fmt.Errorf("unexpected content type: %s", contentType)
165-
}
166-
167-
// Handle any errors that occurred during unmarshaling
168-
if err != nil {
169-
// If unmarshalling fails, check if the content might be HTML
170-
if strings.Contains(string(bodyBytes), "<html>") {
171-
htmlErrorMessage := ExtractErrorMessageFromHTML(string(bodyBytes))
172-
173-
// Log the HTML error message
174-
j.Logger.Warn("Received HTML content instead of expected format",
175-
zap.String("error_message", htmlErrorMessage),
176-
zap.Int("status_code", resp.StatusCode),
177-
)
178-
179-
// Use the HTML error message for logging the error
180-
j.Logger.Error("Unmarshal error with HTML content",
181-
zap.String("error_message", htmlErrorMessage),
182-
zap.Int("status_code", resp.StatusCode),
183-
)
184-
} else {
185-
// If the error is not due to HTML content, log the original error
186-
j.Logger.Error("Unmarshal error",
187-
zap.Error(err),
188-
zap.Int("status_code", resp.StatusCode),
189-
)
190-
}
191-
}
192-
193-
return err
194-
}
195-
196-
// MarshalMultipartFormData takes a map with form fields and file paths and returns the encoded body and content type.
197-
func (j *JamfAPIHandler) MarshalMultipartRequest(fields map[string]string, files map[string]string, log logger.Logger) ([]byte, string, error) {
198-
body := &bytes.Buffer{}
199-
writer := multipart.NewWriter(body)
200-
201-
// Add the simple fields to the form data
202-
for field, value := range fields {
203-
if err := writer.WriteField(field, value); err != nil {
204-
return nil, "", err
205-
}
206-
}
207-
208-
// Add the files to the form data
209-
for formField, filepath := range files {
210-
file, err := os.Open(filepath)
211-
if err != nil {
212-
return nil, "", err
213-
}
214-
defer file.Close()
215-
216-
part, err := writer.CreateFormFile(formField, filepath)
217-
if err != nil {
218-
return nil, "", err
219-
}
220-
if _, err := io.Copy(part, file); err != nil {
221-
return nil, "", err
222-
}
223-
}
224-
225-
// Close the writer before returning
226-
contentType := writer.FormDataContentType()
227-
if err := writer.Close(); err != nil {
228-
return nil, "", err
229-
}
230-
231-
return body.Bytes(), contentType, nil
232-
}
233-
234-
// handleBinaryData checks if the response should be treated as binary data and assigns to out if so.
235-
func (j *JamfAPIHandler) handleBinaryData(contentType, contentDisposition string, bodyBytes []byte, out interface{}) error {
236-
if strings.Contains(contentType, "application/octet-stream") || strings.HasPrefix(contentDisposition, "attachment") {
237-
if outPointer, ok := out.(*[]byte); ok {
238-
*outPointer = bodyBytes
239-
return nil
240-
} else {
241-
return fmt.Errorf("output parameter is not a *[]byte for binary data")
242-
}
243-
}
244-
return nil // If not binary data, no action needed
245-
}

0 commit comments

Comments
 (0)