go-meteologix/httpclient.go

160 lines
4.1 KiB
Go
Raw Normal View History

// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
2023-05-12 12:44:27 +02:00
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"bufio"
"bytes"
"context"
"crypto/tls"
2023-05-14 15:55:03 +02:00
"encoding/json"
2023-05-12 12:44:27 +02:00
"errors"
"fmt"
"io"
"log"
2023-05-12 12:44:27 +02:00
"net/http"
2023-05-15 15:27:09 +02:00
"net/url"
2023-05-12 12:44:27 +02:00
"strings"
"time"
)
const (
// HTTPClientTimeout is the default timeout value for the HTTPClient
HTTPClientTimeout = time.Second * 10
// MIMETypeJSON is a string constant for application/json MIME type
MIMETypeJSON = "application/json"
)
// ErrNonJSONResponse is returned when a HTTPClient request did not return the
// expected application/json content type
var ErrNonJSONResponse = errors.New("HTTP response is of non-JSON content type")
// HTTPClient is a type wrapper for the Go stdlib http.Client and the Config
type HTTPClient struct {
*Config
*http.Client
}
2023-05-14 15:55:03 +02:00
// APIError wraps the error interface for the API
type APIError struct {
Code int `json:"status"`
Details string `json:"detail"`
Message string `json:"message"`
Title string `json:"title"`
Type string `json:"type"`
}
2023-05-12 12:44:27 +02:00
// NewHTTPClient returns a new HTTP client
func NewHTTPClient(c *Config) *HTTPClient {
tc := &tls.Config{
MinVersion: tls.VersionTLS12,
}
ht := &http.Transport{TLSClientConfig: tc}
hc := &http.Client{
Timeout: HTTPClientTimeout,
Transport: ht,
}
2023-05-12 12:44:27 +02:00
return &HTTPClient{c, hc}
}
// Get performs a HTTP GET request for the given URL with the default HTTP timeout
func (hc *HTTPClient) Get(u string) ([]byte, error) {
return hc.GetWithTimeout(u, HTTPClientTimeout)
}
// GetWithTimeout performs a HTTP GET request for the given URL and sets a timeout
// context with the given timeout duration
func (hc *HTTPClient) GetWithTimeout(u string, t time.Duration) ([]byte, error) {
ctx, cfn := context.WithTimeout(context.Background(), t)
defer cfn()
hr, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, err
}
hr.Header.Set("User-Agent", hc.userAgent)
2023-05-12 12:44:27 +02:00
hr.Header.Set("Content-Type", MIMETypeJSON)
hr.Header.Set("Accept", MIMETypeJSON)
hr.Header.Set("Accept-Language", hc.acceptLang)
2023-05-12 12:44:27 +02:00
// User authentication (only required for Meteologix API calls)
if strings.HasPrefix(u, APIBaseURL) {
hc.setAuthentication(hr)
2023-05-12 12:44:27 +02:00
}
sr, err := hc.Do(hr)
if err != nil {
return nil, err
}
if sr == nil {
return nil, errors.New("nil response received")
}
defer func(b io.ReadCloser) {
if err = b.Close(); err != nil {
log.Printf("failed to close HTTP request body: %s", err)
2023-05-12 12:44:27 +02:00
}
}(sr.Body)
2023-05-12 12:44:27 +02:00
if !strings.HasPrefix(sr.Header.Get("Content-Type"), MIMETypeJSON) {
return nil, ErrNonJSONResponse
}
if sr.StatusCode >= http.StatusBadRequest {
ae := new(APIError)
if err = json.NewDecoder(sr.Body).Decode(ae); err != nil {
2023-05-14 15:55:03 +02:00
return nil, fmt.Errorf("failed to unmarshal error JSON: %w", err)
}
if ae.Code < 1 {
ae.Code = sr.StatusCode
}
if ae.Details == "" {
ae.Details = sr.Status
}
return nil, *ae
}
buf := &bytes.Buffer{}
bw := bufio.NewWriter(buf)
_, err = io.Copy(bw, sr.Body)
if err != nil {
return nil, fmt.Errorf("failed to copy HTTP response body to buffer: %w", err)
}
if err = bw.Flush(); err != nil {
return nil, fmt.Errorf("failed to flush buffer: %w", err)
2023-05-14 15:55:03 +02:00
}
2023-05-12 12:44:27 +02:00
return buf.Bytes(), nil
}
// setAuthentication sets the corresponding user authentication header. If an API Key is set, this
// will be preferred, alternatively a username/authPass combination for HTTP Basic auth can
2023-05-12 12:44:27 +02:00
// be used
func (hc *HTTPClient) setAuthentication(hr *http.Request) {
if hc.apiKey != "" {
hr.Header.Set("X-API-Key", hc.Config.apiKey)
2023-05-12 12:44:27 +02:00
return
}
if hc.bearerToken != "" {
hr.Header.Set("Authorization", "Bearer"+hc.bearerToken)
return
}
if hc.authUser != "" && hc.authPass != "" {
2023-05-15 15:27:09 +02:00
hr.SetBasicAuth(url.QueryEscape(hc.authUser), url.QueryEscape(hc.authPass))
2023-05-12 12:44:27 +02:00
}
}
2023-05-14 15:55:03 +02:00
// Error satisfies the error interface for the APIError type
func (e APIError) Error() string {
var em strings.Builder
em.WriteString("API request failed with status HTTP ")
em.WriteString(fmt.Sprintf("%d: ", e.Code))
if e.Details != "" {
em.WriteString(e.Details)
}
if e.Message != "" {
em.WriteString(" (Optional message: ")
em.WriteString(e.Message)
em.WriteString(")")
}
return em.String()
}