File: gotify_send.go

package info (click to toggle)
golang-github-nicholas-fedor-shoutrrr 0.13.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,796 kB
  • sloc: sh: 74; makefile: 62
file content (155 lines) | stat: -rw-r--r-- 4,394 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package gotify

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/nicholas-fedor/shoutrrr/pkg/util/jsonclient"
)

// Sender handles HTTP request execution and response processing.
type Sender interface {
	SendRequest(client *http.Client, url string, request *MessageRequest, headers http.Header) error
}

// DefaultSender provides the default implementation of Sender.
type DefaultSender struct{}

// SendRequest handles the HTTP request.
// This function executes the actual HTTP POST request to the Gotify API endpoint,
// handling both successful responses and error conditions with appropriate error wrapping.
// Parameters:
//   - client: HTTP client to use for the request
//   - url: The complete API endpoint URL to send the request to
//   - request: The JSON payload to send in the request body
//   - headers: Optional headers to set on the request
//
// Returns: error if the request fails or server returns an error, nil on success.
func (s *DefaultSender) SendRequest(
	client *http.Client,
	url string,
	request *MessageRequest,
	headers http.Header,
) error {
	// Prepare response structure to capture API response
	response := &messageResponse{}

	var err error

	if len(headers) == 0 {
		// Use JSON client for standard requests - this will handle error extraction
		jsonClient := jsonclient.NewWithHTTPClient(client)

		err = jsonClient.Post(url, request, response)
		if err != nil {
			// Try to extract structured error
			errorRes := &responseError{}
			if jsonClient.ErrorResponse(err, errorRes) {
				return fmt.Errorf("server error: %w", errorRes)
			}

			return fmt.Errorf("%s: %w", ErrSendFailed.Error(), err)
		}

		return nil
	}

	// Use direct HTTP client when custom headers are needed
	body, err := s.sendRequestWithHeaders(client, url, request, headers)
	if err != nil {
		return err
	}

	err = json.Unmarshal(body, response)
	if err != nil {
		return fmt.Errorf("%s: %w", ErrParseResponse.Error(), err)
	}

	return nil
}

// sendRequestWithHeaders sends a request with custom headers using the underlying HTTP client.
// This method is used when per-request headers are needed, bypassing the jsonclient
// to avoid modifying shared header state.
// Parameters:
//   - client: HTTP client to use
//   - url: The complete API endpoint URL to send the request to
//   - request: The JSON payload to send in the request body
//   - headers: Custom headers to set on the request
//
// Returns: the response body as bytes if successful, or an error.
func (s *DefaultSender) sendRequestWithHeaders(
	client *http.Client,
	url string,
	request *MessageRequest,
	headers http.Header,
) ([]byte, error) {
	body, err := json.Marshal(request)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", ErrMarshalRequest.Error(), err)
	}

	req, err := http.NewRequestWithContext(
		context.Background(),
		http.MethodPost,
		url,
		bytes.NewReader(body),
	)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", ErrCreateRequest.Error(), err)
	}

	s.setRequestHeaders(req, headers)

	res, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", ErrSendRequest.Error(), err)
	}

	defer func() { _ = res.Body.Close() }()

	body, err = io.ReadAll(res.Body)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", ErrReadResponse.Error(), err)
	}

	if err := s.handleResponseError(res, body); err != nil {
		return nil, err
	}

	return body, nil
}

// setRequestHeaders sets the Content-Type and custom headers on the HTTP request.
func (s *DefaultSender) setRequestHeaders(req *http.Request, headers http.Header) {
	req.Header.Set("Content-Type", "application/json")

	for key, values := range headers {
		for _, value := range values {
			req.Header.Add(key, value)
		}
	}
}

// handleResponseError checks the response status and extracts error information if present.
func (s *DefaultSender) handleResponseError(res *http.Response, body []byte) error {
	if res.StatusCode >= 400 { //nolint:mnd
		errorRes := &responseError{}
		if s.extractErrorResponse(body, errorRes) {
			return fmt.Errorf("server error: %w", errorRes)
		}

		return fmt.Errorf("%w: %v", ErrUnexpectedStatus, res.Status)
	}

	return nil
}

// extractErrorResponse attempts to extract a structured error from a failed request.
func (s *DefaultSender) extractErrorResponse(body []byte, errorRes *responseError) bool {
	return json.Unmarshal(body, errorRes) == nil
}