mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
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:
4 files changed
+106
-59
No files matched your search
@@ -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.
|
||||
|
||||
@@ -232,12 +232,11 @@ func TestAPIErrorLookupRepeatsReadsWithTheFailedCredential(t *testing.T) {
|
||||
var apiErr *APIError
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, server.URL+"/base", apiErr.Instance)
|
||||
var repo struct {
|
||||
FullName string `json:"full_name"`
|
||||
HasIssues bool `json:"has_issues"`
|
||||
}
|
||||
require.Equal(t, http.StatusOK, apiErr.Lookup(context.Background(), "repos/a/b", &repo))
|
||||
require.Equal(t, "a/b", repo.FullName)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.Lookup(context.Background(), "repos/a/missing", nil))
|
||||
require.Zero(t, (&APIError{Status: http.StatusNotFound}).Lookup(context.Background(), "repos/a/b", nil), "errors built without a request cannot look up")
|
||||
lookup := apiErr.LookupClient()
|
||||
require.NotNil(t, lookup)
|
||||
resp, err := lookup.Get(server.URL + "/base/api/v1/repos/a/b")
|
||||
require.NoError(t, err)
|
||||
_ = resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Nil(t, (&APIError{Status: http.StatusNotFound}).LookupClient(), "errors built without a request cannot look up")
|
||||
}
|
||||
Reference in new issue
Block a user