package cast import ( "bytes" "context" "encoding/json" "io" "log/slog" "net/http" "strconv" "time" ) type Client struct { baseURL string apiKey string senderID string httpClient *http.Client } func NewClient(baseURL, apiKey, senderID string) *Client { return &Client{ baseURL: baseURL, apiKey: apiKey, senderID: senderID, httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } func (c *Client) SendSMS(ctx context.Context, to, message string) (*SendResponse, error) { req := SendRequest{To: to, Message: message} if c.senderID != "" { req.SenderID = c.senderID } body, err := json.Marshal(req) if err != nil { return nil, err } const maxRetries = 3 backoff := []time.Duration{1 * time.Second, 2 * time.Second, 4 * time.Second} for attempt := 0; attempt <= maxRetries; attempt++ { resp, err := c.doRequest(ctx, body) if err != nil { return nil, err } if resp.StatusCode == http.StatusTooManyRequests { if attempt == maxRetries { return nil, &CastAPIError{StatusCode: resp.StatusCode, APIError: "rate limited, max retries exceeded"} } wait := backoff[attempt] if ra := resp.Header.Get("Retry-After"); ra != "" { if secs, err := strconv.ParseFloat(ra, 64); err == nil { wait = time.Duration(secs * float64(time.Second)) } } slog.Warn("cast api rate limited, retrying", "attempt", attempt+1, "wait", wait) resp.Body.Close() select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(wait): } continue } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { var errResp SendResponse _ = json.Unmarshal(data, &errResp) return nil, &CastAPIError{StatusCode: resp.StatusCode, APIError: errResp.Error} } var result SendResponse if err := json.Unmarshal(data, &result); err != nil { return nil, err } if !result.Success { return nil, &CastAPIError{StatusCode: resp.StatusCode, APIError: result.Error} } return &result, nil } return nil, &CastAPIError{StatusCode: http.StatusTooManyRequests, APIError: "max retries exceeded"} } func (c *Client) doRequest(ctx context.Context, body []byte) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/sms/send", bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("X-API-Key", c.apiKey) req.Header.Set("Content-Type", "application/json") return c.httpClient.Do(req) }