-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.go
More file actions
169 lines (151 loc) · 4.66 KB
/
client.go
File metadata and controls
169 lines (151 loc) · 4.66 KB
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package checkhost
import (
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// maybeDecompress checks for a gzip magic header (0x1f 0x8b) or a
// Content-Encoding: gzip response header and inflates the body if either
// is present. The Check-Host API edge compresses large JSON bodies (e.g.
// /locations) regardless of the Accept-Encoding request header.
func maybeDecompress(body []byte, encoding string) []byte {
gzippedByMagic := len(body) >= 2 && body[0] == 0x1f && body[1] == 0x8b
gzippedByHeader := strings.EqualFold(encoding, "gzip")
if !gzippedByMagic && !gzippedByHeader {
return body
}
gzr, err := gzip.NewReader(bytes.NewReader(body))
if err != nil {
return body
}
defer gzr.Close()
dec, err := io.ReadAll(gzr)
if err != nil {
return body
}
return dec
}
const (
DefaultBaseURL = "https://api.check-host.cc"
)
// CheckHostException mapped as a generic Go error.
var (
ErrRateLimit = errors.New("rate limit reached, please provide an API key or slow down your requests")
ErrBadRequest = errors.New("problem with your input parameters, please check your payload")
ErrServerError = errors.New("internal server error, please try again later")
)
type CheckHost struct {
APIKey string
BaseURL string
HTTPClient *http.Client
}
// NewClient creates a new CheckHost client. The apiKey is optional.
func NewClient(apiKey string) *CheckHost {
return &CheckHost{
APIKey: apiKey,
BaseURL: DefaultBaseURL,
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// doRequest performs the HTTP request and unmarshals the response.
func (c *CheckHost) doRequest(method, path string, body interface{}, response interface{}) error {
var buf io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("error marshaling request body: %w", err)
}
buf = bytes.NewBuffer(b)
}
url := fmt.Sprintf("%s/%s", c.BaseURL, path)
req, err := http.NewRequest(method, url, buf)
if err != nil {
return fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("network error occurred during request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error reading response body: %w", err)
}
respBody = maybeDecompress(respBody, resp.Header.Get("Content-Encoding"))
// Handle specific HTTP Status Codes based on check-host API spec
switch resp.StatusCode {
case 200:
// Success
case 400:
return fmt.Errorf("%w: %s", ErrBadRequest, string(respBody))
case 429:
return fmt.Errorf("%w: %s", ErrRateLimit, string(respBody))
case 500:
return fmt.Errorf("%w: %s", ErrServerError, string(respBody))
default:
if resp.StatusCode >= 400 {
return fmt.Errorf("check-host api returned an unexpected status code %d: %s", resp.StatusCode, string(respBody))
}
}
// Unmarshal JSON to the provided response pointer, if any
if response != nil && len(respBody) > 0 {
err = json.Unmarshal(respBody, response)
if err != nil {
// Some endpoints like /myip simply return a string text/plain
if strResp, ok := response.(*string); ok {
*strResp = string(respBody)
return nil
}
return fmt.Errorf("error parsing response as json: %w. response text: %s", err, string(respBody))
}
}
return nil
}
// doRequestRaw performs a GET request and returns the raw response body
// without trying to JSON-decode it. Used for binary endpoints
// (og-image PNG, country-map PNG/SVG).
func (c *CheckHost) doRequestRaw(path, accept string) ([]byte, error) {
url := fmt.Sprintf("%s/%s", c.BaseURL, path)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
if accept == "" {
accept = "*/*"
}
req.Header.Set("Accept", accept)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("network error occurred during request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %w", err)
}
// Don't decompress binary responses (PNG / SVG) - the body IS the image.
switch resp.StatusCode {
case 200:
return body, nil
case 400:
return nil, fmt.Errorf("%w: %s", ErrBadRequest, string(body))
case 429:
return nil, fmt.Errorf("%w: %s", ErrRateLimit, string(body))
case 500:
return nil, fmt.Errorf("%w: %s", ErrServerError, string(body))
default:
return nil, fmt.Errorf("check-host api returned an unexpected status code %d: %s", resp.StatusCode, string(body))
}
}