From 9b2c1e5cebff255e716bf422889b2486670782e1 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 15 Jan 2026 19:08:28 -0600 Subject: [PATCH] transport: Moved transport/http.go upstream to feedkit (was previously in weatherfeeder). --- transport/http.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 transport/http.go diff --git a/transport/http.go b/transport/http.go new file mode 100644 index 0000000..d844516 --- /dev/null +++ b/transport/http.go @@ -0,0 +1,70 @@ +// 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 +}