mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
fix: address verified review findings (api-transport-download-1)
Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
11 files changed
+192
-11
No files matched your search
@@ -6,6 +6,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
@@ -110,7 +111,8 @@ func (s API) Visit(ctx context.Context, options APIOptions, visit func(*http.Res
|
||||
}
|
||||
seen[absolute] = true
|
||||
current, _ := url.Parse(absolute)
|
||||
response, err := s.Client.Send(ctx, options.Method, endpoint, options.Body, options.Headers)
|
||||
captureCtx, failure := transport.CaptureErrors(ctx)
|
||||
response, err := s.Client.Send(captureCtx, options.Method, endpoint, options.Body, options.Headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,6 +126,9 @@ func (s API) Visit(ctx context.Context, options APIOptions, visit func(*http.Res
|
||||
return closeErr
|
||||
}
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
if captured := failure(); captured != nil {
|
||||
return captured // carries the server message even when the visitor wrote the body to a file
|
||||
}
|
||||
return &transport.APIError{Status: response.StatusCode, Method: options.Method, Endpoint: current.Path}
|
||||
}
|
||||
if !options.Paginate {
|
||||
@@ -146,13 +151,28 @@ func (s API) Visit(ctx context.Context, options APIOptions, visit func(*http.Res
|
||||
if page+1 >= options.MaxPages {
|
||||
return ErrMaxPages
|
||||
}
|
||||
if _, err := s.Client.URL(next); err != nil {
|
||||
return err
|
||||
}
|
||||
relative, err := url.Parse(next)
|
||||
if err != nil || strings.Contains(next, "\\") {
|
||||
return errors.New("API returned an invalid pagination link")
|
||||
}
|
||||
if _, err := s.Client.URL(next); err != nil {
|
||||
// Gitea builds links from its ROOT_URL, which can name the instance differently than the
|
||||
// configured host. Take the page from the link's query and keep the request on this instance.
|
||||
if !relative.IsAbs() || apiTail(relative.Path) == "" || apiTail(relative.Path) != apiTail(current.Path) {
|
||||
return fmt.Errorf("%w (next page link points to %s, the request went to %s)", err, relative.Host, current.Host)
|
||||
}
|
||||
same := *current
|
||||
same.RawQuery = relative.RawQuery
|
||||
relative = &same
|
||||
}
|
||||
endpoint = current.ResolveReference(relative).String()
|
||||
}
|
||||
}
|
||||
|
||||
// apiTail returns the part of a path from its /api/ segment on.
|
||||
func apiTail(path string) string {
|
||||
if i := strings.Index(path, "/api/"); i >= 0 {
|
||||
return path[i:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -39,3 +39,43 @@ func TestAPIPaginationRejectsUnsafeAndRepeatingLinks(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"https://example.org/items?a=1,2"}, links)
|
||||
}
|
||||
|
||||
func TestAPIPaginationFollowsLinksNamingTheInstanceDifferently(t *testing.T) {
|
||||
var pages []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
pages = append(pages, r.Host+r.URL.RequestURI())
|
||||
if r.URL.Query().Get("page") != "2" {
|
||||
// Gitea builds Link headers from its ROOT_URL rather than from the request host.
|
||||
w.Header().Set("Link", `<https://git.example.test/api/v1/items?limit=1&page=2>; rel="next"`)
|
||||
}
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
visit := func(response *http.Response) error { _, err := io.Copy(io.Discard, response.Body); return err }
|
||||
err := (API{Client: transport.New(server.URL, "secret", false)}).Visit(context.Background(), APIOptions{Endpoint: "items", Method: "GET", Paginate: true}, visit)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pages, 2)
|
||||
require.Contains(t, pages[1], "/api/v1/items?limit=1&page=2")
|
||||
require.Contains(t, pages[1], server.Listener.Addr().String(), "the second page still goes to the configured host")
|
||||
|
||||
err = (API{Client: transport.New(server.URL, "secret", false)}).Visit(context.Background(), APIOptions{Endpoint: "other", Method: "GET", Paginate: true}, visit)
|
||||
require.ErrorContains(t, err, "git.example.test")
|
||||
}
|
||||
|
||||
func TestAPIVisitReturnsTheServerMessageOfAFailedResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
_, _ = w.Write([]byte(`{"message":"invalid color: #nothex"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
var body []byte
|
||||
err := (API{Client: transport.New(server.URL, "secret", false)}).Visit(context.Background(), APIOptions{Endpoint: "labels", Method: "POST"}, func(response *http.Response) error {
|
||||
var err error
|
||||
body, err = io.ReadAll(response.Body)
|
||||
return err
|
||||
})
|
||||
var apiErr *transport.APIError
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, "invalid color: #nothex", apiErr.Message)
|
||||
require.Contains(t, string(body), "nothex", "the visitor still sees the whole body")
|
||||
}
|
||||
Reference in new issue
Block a user