Skip to content

Commit

Permalink
Extract additional info from HTTP response into the returned error (#418
Browse files Browse the repository at this point in the history
)

This provides the caller additional info, in particular about rate
limits. (Unless they're using `util.RobustHTTPClient()`)
  • Loading branch information
ericvolp12 authored Nov 16, 2023
2 parents cbc8356 + d3ef676 commit 236c615
Showing 1 changed file with 65 additions and 2 deletions.
67 changes: 65 additions & 2 deletions xrpc/xrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"

"github.com/bluesky-social/indigo/util"
"github.com/carlmjohnson/versioninfo"
)

type Client struct {
// Client is an HTTP client to use. If not set, defaults to http.RobustHTTPClient().
// Note that http.RobustHTTPClient() swallows retryable errors (including hitting a rate limit),
// not allowing your code to handle them differently.
Client *http.Client
Auth *AuthInfo
AdminToken *string
Expand Down Expand Up @@ -49,6 +54,64 @@ func (xe *XRPCError) Error() string {
return fmt.Sprintf("%s: %s", xe.ErrStr, xe.Message)
}

type Error struct {
StatusCode int
Wrapped error
Ratelimit *RatelimitInfo
}

func (e *Error) Error() string {
// Preserving "XRPC ERROR %d" prefix for compatibility - previously matching this string was the only way
// to obtain the status code.
if e.Wrapped == nil {
return fmt.Sprintf("XRPC ERROR %d", e.StatusCode)
}
if e.StatusCode == http.StatusTooManyRequests && e.Ratelimit != nil {
return fmt.Sprintf("XRPC ERROR %d: %s (throttled until %s)", e.StatusCode, e.Wrapped, e.Ratelimit.Reset.Local())
}
return fmt.Sprintf("XRPC ERROR %d: %s", e.StatusCode, e.Wrapped)
}

func (e *Error) Unwrap() error {
if e.Wrapped == nil {
return nil
}
return e.Wrapped
}

func (e *Error) IsThrottled() bool {
return e.StatusCode == http.StatusTooManyRequests
}

func errorFromHTTPResponse(resp *http.Response, err error) error {
r := &Error{
StatusCode: resp.StatusCode,
Wrapped: err,
}
if resp.Header.Get("ratelimit-limit") != "" {
r.Ratelimit = &RatelimitInfo{
Policy: resp.Header.Get("ratelimit-policy"),
}
if n, err := strconv.ParseInt(resp.Header.Get("ratelimit-reset"), 10, 64); err == nil {
r.Ratelimit.Reset = time.Unix(n, 0)
}
if n, err := strconv.ParseInt(resp.Header.Get("ratelimit-limit"), 10, 64); err == nil {
r.Ratelimit.Limit = int(n)
}
if n, err := strconv.ParseInt(resp.Header.Get("ratelimit-remaining"), 10, 64); err == nil {
r.Ratelimit.Remaining = int(n)
}
}
return r
}

type RatelimitInfo struct {
Limit int
Remaining int
Policy string
Reset time.Time
}

const (
Query = XRPCRequestType(iota)
Procedure
Expand Down Expand Up @@ -137,9 +200,9 @@ func (c *Client) Do(ctx context.Context, kind XRPCRequestType, inpenc string, me
if resp.StatusCode != 200 {
var xe XRPCError
if err := json.NewDecoder(resp.Body).Decode(&xe); err != nil {
return fmt.Errorf("failed to decode xrpc error message (status: %d): %w", resp.StatusCode, err)
return errorFromHTTPResponse(resp, fmt.Errorf("failed to decode xrpc error message: %w", err))
}
return fmt.Errorf("XRPC ERROR %d: %w", resp.StatusCode, &xe)
return errorFromHTTPResponse(resp, &xe)
}

if out != nil {
Expand Down

0 comments on commit 236c615

Please sign in to comment.