Files
jellybit/internal/metadata/http.go
T

95 lines
3.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package metadata
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const defaultTimeout = 10 * time.Second
// newHTTPClient собирает http.Client с опциональным прокси и таймаутом.
func newHTTPClient(proxy string, timeout time.Duration) (*http.Client, error) {
if timeout <= 0 {
timeout = defaultTimeout
}
transport := http.DefaultTransport
if proxy != "" {
u, err := url.Parse(proxy)
if err != nil {
return nil, fmt.Errorf("metadata: parse proxy %q: %w", proxy, err)
}
// Клонируем дефолтный транспорт (dial/TLS-таймауты, keep-alive), а не
// собираем голый — иначе при живом-но-залипшем прокси полагались бы
// только на общий Client.Timeout. Он остаётся верхней границей запроса.
t := http.DefaultTransport.(*http.Transport).Clone()
t.Proxy = http.ProxyURL(u)
transport = t
}
return &http.Client{Timeout: timeout, Transport: transport}, nil
}
const maxBody = 4 << 20 // 4 MiB — потолок на тело ответа
// getJSON выполняет GET и декодирует JSON-ответ в out. headers — опц.
// дополнительные заголовки (напр. Authorization).
func getJSON(ctx context.Context, hc *http.Client, rawURL string, headers map[string]string, out any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil {
return fmt.Errorf("metadata: build request: %w", err)
}
req.Header.Set("Accept", "application/json")
for k, v := range headers {
req.Header.Set(k, v)
}
return doJSON(hc, req, out)
}
// postJSON выполняет POST с JSON-телом и декодирует ответ.
func postJSON(ctx context.Context, hc *http.Client, rawURL string, body, out any) error {
payload, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("metadata: marshal body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, rawURL, bytes.NewReader(payload))
if err != nil {
return fmt.Errorf("metadata: build request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
return doJSON(hc, req, out)
}
func doJSON(hc *http.Client, req *http.Request, out any) error {
resp, err := hc.Do(req)
if err != nil {
return fmt.Errorf("metadata: request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxBody))
if err != nil {
return fmt.Errorf("metadata: read body: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("metadata: status %d: %s", resp.StatusCode, snippet(raw))
}
if err := json.Unmarshal(raw, out); err != nil {
return fmt.Errorf("metadata: decode: %w (body: %s)", err, snippet(raw))
}
return nil
}
func snippet(b []byte) string {
const max = 200
if len(b) > max {
return string(b[:max]) + "…"
}
return string(b)
}