File size: 3,126 Bytes
04f1444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// Code generated by @openmeter/typespec-go. DO NOT EDIT.

package openmeter

import (
	"encoding/json"
	"errors"
	"fmt"
)

// ErrEmptyID is returned by operations that target a single resource when the
// resource ID is empty. It is caught before any request is made so an omitted
// ID surfaces as a clear client-side error rather than an ambiguous server
// response. Match it with errors.Is.
var ErrEmptyID = errors.New("openmeter: resource ID must not be empty")

// APIError is returned for any non-2xx API response. It mirrors the API's
// RFC 7807-style problem body. When the body cannot be parsed as such, Title is
// left empty and RawBody carries the undecoded payload.
type APIError struct {
	// StatusCode is the HTTP status code of the response.
	StatusCode int `json:"-"`

	// Status is the status code echoed in the problem body (usually equal to
	// StatusCode).
	Status int `json:"status"`
	// Title is a short, stable, human-readable summary of the problem.
	Title string `json:"title"`
	// Type is an optional machine-readable error type.
	Type string `json:"type,omitempty"`
	// Detail is a human-readable explanation specific to this occurrence.
	Detail string `json:"detail"`
	// Instance carries the correlation ID, formatted as kong:trace:<id>.
	Instance string `json:"instance"`

	// RawBody is the undecoded response body, always populated.
	RawBody []byte `json:"-"`
}

func newAPIError(statusCode int, body []byte) *APIError {
	e := &APIError{StatusCode: statusCode, RawBody: body}
	// Best-effort decode; a non-conforming body still yields a useful error via
	// StatusCode and RawBody.
	_ = json.Unmarshal(body, e)
	return e
}

// AsAPIError returns the APIError inside err, when err came from an API response.
func AsAPIError(err error) (*APIError, bool) {
	var apiErr *APIError
	if errors.As(err, &apiErr) {
		return apiErr, true
	}
	return nil, false
}

// Decode decodes the original error response body into out.
func (e *APIError) Decode(out any) error {
	return json.Unmarshal(e.RawBody, out)
}

// DecodeAPIError decodes an API error body into T. The returned boolean is false
// when err is not an APIError.
func DecodeAPIError[T any](err error) (T, bool, error) {
	var zero T
	apiErr, ok := AsAPIError(err)
	if !ok {
		return zero, false, nil
	}

	var out T
	if err := apiErr.Decode(&out); err != nil {
		return zero, true, err
	}
	return out, true, nil
}

func (e *APIError) Error() string {
	switch {
	case e.Title != "" && e.Detail != "":
		return fmt.Sprintf("openmeter: %d %s: %s", e.StatusCode, e.Title, e.Detail)
	case e.Title != "":
		return fmt.Sprintf("openmeter: %d %s", e.StatusCode, e.Title)
	default:
		// No RFC 7807 fields parsed (e.g. a proxy returned an HTML error page).
		// Inline the raw body for diagnostics but bound it so a large payload
		// can't blow up log lines; RawBody still holds the full response.
		const maxInline = 512
		body := e.RawBody
		suffix := ""
		if len(body) > maxInline {
			body = body[:maxInline]
			suffix = "… (truncated)"
		}
		return fmt.Sprintf("openmeter: unexpected status %d: %s%s", e.StatusCode, string(body), suffix)
	}
}