refactor(errors): explain failed requests through the SDK

The hints for a missing repository, a pull request number that names an
issue and an existing pull request read the API with raw GETs from the app
layer. Read them through SDK lookups in services, which reuse the failed
request's instance and credential, and let transport only hand out that
client.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-12 09:57:12 +02:00
1 parent 03aa64233f
commit 5fd4be9803
4 files changed
+106 -59

No files matched your search

+9 -28
View File
@@ -35,9 +35,8 @@ type APIError struct {
// Instance is the installation URL the failed request was sent to, when it was recorded
// from a request.
Instance string
// apiBase and lookup repeat reads on the failed request's instance with its credential.
apiBase string
lookup http.RoundTripper
// lookup repeats reads on the failed request's instance with its credential.
lookup http.RoundTripper
}
func (e *APIError) Error() string {
@@ -70,46 +69,28 @@ func (e *APIError) request() string {
}
// bind records the instance of the request to u and the credentialed transport rt, so the
// failure can later be explained with Lookup.
// failure can later be explained with LookupClient.
func (e *APIError) bind(u *url.URL, rt http.RoundTripper) {
prefix, _, ok := strings.Cut(u.Path, "/api/v1/")
if !ok {
return
}
instance := url.URL{Scheme: u.Scheme, Host: u.Host, Path: prefix}
e.Instance = instance.String()
instance.Path += "/api/v1/"
e.apiBase, e.lookup = instance.String(), rt
e.Instance, e.lookup = instance.String(), rt
}
// Lookup reads endpoint, a path below the API base such as "repos/alice/project", with the
// instance and credential of the request that failed, and decodes a successful JSON response
// into out. It returns the response status, or 0 when the error was not recorded from a
// request or the lookup itself failed.
func (e *APIError) Lookup(ctx context.Context, endpoint string, out any) int {
// LookupClient returns an HTTP client that repeats reads on the instance of the failed request,
// Instance, with its credential, or nil when the error was not recorded from a request.
func (e *APIError) LookupClient() *http.Client {
if e.lookup == nil {
return 0
return nil
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, e.apiBase+strings.TrimLeft(endpoint, "/"), nil)
if err != nil {
return 0
}
req.Header.Set("Accept", "application/json")
resp, err := e.lookup.RoundTrip(req)
if err != nil {
return 0
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 && out != nil && json.NewDecoder(io.LimitReader(resp.Body, maxLookupBody)).Decode(out) != nil {
return 0
}
return resp.StatusCode
return &http.Client{Transport: e.lookup, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
}
const (
maxErrorBody = 4 << 10
maxErrorMessage = 300
maxLookupBody = 1 << 20
)
// newAPIError keeps only a bounded, printable JSON "message" field of a failed response body.