spogo/internal/spotify/client_request.go
2026-01-03 02:04:16 +01:00

134 lines
3.6 KiB
Go

package spotify
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
func (c *Client) get(ctx context.Context, path string, params url.Values, dest any) error {
return c.send(ctx, http.MethodGet, path, params, nil, dest)
}
func (c *Client) put(ctx context.Context, path string, payload any) error {
return c.send(ctx, http.MethodPut, path, nil, payload, nil)
}
func (c *Client) post(ctx context.Context, path string, payload any) error {
return c.send(ctx, http.MethodPost, path, nil, payload, nil)
}
func (c *Client) postJSON(ctx context.Context, path string, payload any, dest any) error {
return c.send(ctx, http.MethodPost, path, nil, payload, dest)
}
func (c *Client) putParams(ctx context.Context, path string, params url.Values) error {
return c.send(ctx, http.MethodPut, path, params, nil, nil)
}
func (c *Client) postParams(ctx context.Context, path string, params url.Values) error {
return c.send(ctx, http.MethodPost, path, params, nil, nil)
}
func (c *Client) send(ctx context.Context, method, path string, params url.Values, payload any, dest any) error {
const (
maxAttempts = 3
maxRetryDelay = 3 * time.Second
)
for attempt := 0; attempt < maxAttempts; attempt++ {
requestURL := c.baseURL + path
if params == nil {
if c.market != "" || c.language != "" || ((method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete) && c.device != "") {
params = url.Values{}
}
}
if params != nil {
if c.market != "" && params.Get("market") == "" {
params.Set("market", c.market)
}
if c.language != "" && params.Get("locale") == "" {
params.Set("locale", c.language)
}
if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete {
if c.device != "" && params.Get("device_id") == "" {
params.Set("device_id", c.device)
}
}
if encoded := params.Encode(); encoded != "" {
requestURL += "?" + encoded
}
}
var body io.Reader
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return err
}
body = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, requestURL, body)
if err != nil {
return err
}
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
token, err := c.token(ctx)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", defaultUserAgent())
resp, err := c.client.Do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < maxAttempts-1 {
retryAfter := time.Second
if header := resp.Header.Get("Retry-After"); header != "" {
if seconds, err := strconv.Atoi(header); err == nil && seconds > 0 {
retryAfter = time.Duration(seconds) * time.Second
}
}
if retryAfter > maxRetryDelay {
retryAfter = maxRetryDelay
}
_ = resp.Body.Close()
c.mu.Lock()
c.lastToken = Token{}
c.mu.Unlock()
select {
case <-time.After(retryAfter):
continue
case <-ctx.Done():
return ctx.Err()
}
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode == http.StatusNoContent {
if dest != nil {
return ErrNoContent
}
return nil
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return apiErrorFromResponse(resp)
}
if dest == nil {
return nil
}
if resp.ContentLength == 0 {
return nil
}
return json.NewDecoder(resp.Body).Decode(dest)
}
return errors.New("spotify api error (429): rate limit retry exhausted")
}