71 lines
1.7 KiB
Go
71 lines
1.7 KiB
Go
// FILE: ./transport/http.go
|
|
package transport
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// maxResponseBodyBytes is a hard safety limit on HTTP response bodies.
|
|
// API responses should be small, so this protects us from accidental
|
|
// or malicious large responses.
|
|
const maxResponseBodyBytes = 2 << 21 // 4 MiB
|
|
|
|
// DefaultHTTPTimeout is the standard timeout used by weatherfeeder HTTP sources.
|
|
// Individual drivers may override this if they have a specific need.
|
|
const DefaultHTTPTimeout = 10 * time.Second
|
|
|
|
// NewHTTPClient returns a simple http.Client configured with a timeout.
|
|
// If timeout <= 0, DefaultHTTPTimeout is used.
|
|
func NewHTTPClient(timeout time.Duration) *http.Client {
|
|
if timeout <= 0 {
|
|
timeout = DefaultHTTPTimeout
|
|
}
|
|
return &http.Client{Timeout: timeout}
|
|
}
|
|
|
|
func FetchBody(ctx context.Context, client *http.Client, url, userAgent, accept string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if userAgent != "" {
|
|
req.Header.Set("User-Agent", userAgent)
|
|
}
|
|
if accept != "" {
|
|
req.Header.Set("Accept", accept)
|
|
}
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("HTTP %s", res.Status)
|
|
}
|
|
|
|
// Read at most maxResponseBodyBytes + 1 so we can detect overflow.
|
|
limited := io.LimitReader(res.Body, maxResponseBodyBytes+1)
|
|
|
|
b, err := io.ReadAll(limited)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(b) == 0 {
|
|
return nil, fmt.Errorf("empty response body")
|
|
}
|
|
|
|
if len(b) > maxResponseBodyBytes {
|
|
return nil, fmt.Errorf("response body too large (>%d bytes)", maxResponseBodyBytes)
|
|
}
|
|
|
|
return b, nil
|
|
}
|