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) } transport = &http.Transport{Proxy: http.ProxyURL(u)} } 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) }