mirror of
https://github.com/distribution/distribution.git
synced 2026-10-28 10:15:27 +08:00
fix(proxy): clamp oversized n query param instead of returning 400
PR #4353 made MaxTags (default 1000) a hard ceiling on the `n` query parameter — anything larger and the handler returns 400 PAGINATION_NUMBER_INVALID before the request ever reaches storage or the proxy tag service. That broke clients like Renovate which use n=10000 against pull-through caches. #4846 fixed a related 500 in proxy mode but not this 400, so users reported the regression still persisted. The OCI distribution-spec describes pagination differently: a server MAY return fewer than `n` results "when the total number of tags attached to the repository is less than <int> or a Link header is provided" — otherwise it MUST include `<int>` results. In other words, the right answer for "client asked for more than we'll serve" is `maxtags` results plus a Link header, not a rejection. PAGINATION_NUMBER_INVALID isn't among the 14 error codes the spec defines, either. Drop the oversized-n rejection and clamp to MaxTags instead; the existing Link-header path already handles continuation correctly. Malformed (non-integer) and negative `n` values keep returning 400, since the spec defines `n` as a non-negative integer and those requests are genuinely invalid. Verified end-to-end against registry-1.docker.io in proxy mode: n=10000 now returns the tag list (or a clamped page with Link) instead of 400. Also restores pre-3.1.0 behavior for Renovate-style clients without needing proxy-specific logic. Spec reference: https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-tags Signed-off-by: Milos Gajdos <milosthegajdos@gmail.com>
This commit is contained in:
3 files changed
+68
-7
No files matched your search
@@ -805,8 +805,10 @@ location of a proxy for the layer stored by the S3 storage driver.
|
||||
## `tags`
|
||||
|
||||
The `tags` subsection provides configuration to limit the maximum number of tags
|
||||
returned by the tags API endpoint. When a client requests the list of tags for
|
||||
a repository, the registry will return at most `maxtags` tags.
|
||||
returned in a single response from the tags API endpoint. When a client requests
|
||||
more than `maxtags` via the `n` query parameter, the server returns `maxtags`
|
||||
results along with a `Link` header so the client can paginate to retrieve the
|
||||
rest.
|
||||
|
||||
```yaml
|
||||
tags:
|
||||
|
||||
@@ -612,6 +612,63 @@ func TestTagsAPI(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTagsAPIMaxTagsClamp verifies that the tags/list endpoint clamps an n
|
||||
// query parameter greater than the server's configured MaxTags down to MaxTags
|
||||
// rather than returning 400 — the OCI distribution-spec permits a server to
|
||||
// return fewer than n results when a Link header is provided.
|
||||
func TestTagsAPIMaxTagsClamp(t *testing.T) {
|
||||
config := configuration.Configuration{
|
||||
Storage: configuration.Storage{
|
||||
"inmemory": configuration.Parameters{},
|
||||
"maintenance": configuration.Parameters{"uploadpurging": map[any]any{"enabled": false}},
|
||||
},
|
||||
Catalog: configuration.Catalog{MaxEntries: 5},
|
||||
Tags: configuration.Tags{MaxTags: 2},
|
||||
}
|
||||
config.HTTP.Headers = headerConfig
|
||||
env := newTestEnvWithConfig(t, &config)
|
||||
defer env.Shutdown()
|
||||
|
||||
imageName, err := reference.WithName("test")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to parse reference: %v", err)
|
||||
}
|
||||
|
||||
tags := []string{"2j2ar", "asj9e", "jyi7b", "kb0j5", "sb71y"}
|
||||
for _, tag := range tags {
|
||||
createRepository(env, t, imageName.Name(), tag)
|
||||
}
|
||||
|
||||
tagsURL, err := env.builder.BuildTagsURL(imageName, url.Values{"n": []string{"100"}})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error building tags URL: %v", err)
|
||||
}
|
||||
|
||||
resp, err := http.Get(tagsURL)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error issuing request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected response status code to be %d, got %d", http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
var body tagsAPIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("unexpected error decoding response body: %v", err)
|
||||
}
|
||||
expected := tagsAPIResponse{Name: imageName.Name(), Tags: []string{"2j2ar", "asj9e"}}
|
||||
if !reflect.DeepEqual(body, expected) {
|
||||
t.Fatalf("expected response body to be:\n%+v\ngot:\n%+v", expected, body)
|
||||
}
|
||||
|
||||
wantLink := `</v2/test/tags/list?last=asj9e&n=2>; rel="next"`
|
||||
if got := resp.Header.Get("Link"); got != wantLink {
|
||||
t.Fatalf("expected response Link header to be %q, got %q", wantLink, got)
|
||||
}
|
||||
}
|
||||
|
||||
func checkLink(t *testing.T, urlStr string, numEntries int, last string) url.Values {
|
||||
re := regexp.MustCompile("<(/v2/_catalog.*)>; rel=\"next\"")
|
||||
matches := re.FindStringSubmatch(urlStr)
|
||||
|
||||
@@ -41,17 +41,19 @@ func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
limit := -1
|
||||
|
||||
// parse n, if n unparseable, or negative assign it to defaultReturnedEntries
|
||||
if n := q.Get("n"); n != "" {
|
||||
if th.App.Config.Tags.MaxTags > 0 {
|
||||
limit = th.App.Config.Tags.MaxTags
|
||||
}
|
||||
parsedMax, err := strconv.Atoi(n)
|
||||
if err != nil || (limit > 0 && parsedMax > limit) || parsedMax < 0 {
|
||||
if err != nil || parsedMax < 0 {
|
||||
th.Errors = append(th.Errors, errcode.ErrorCodePaginationNumberInvalid.WithDetail(map[string]int{"n": parsedMax}))
|
||||
return
|
||||
}
|
||||
limit = parsedMax
|
||||
// Per the OCI distribution-spec, a server MAY return fewer than n
|
||||
// results when a Link header is provided for continuation. Clamp to
|
||||
// MaxTags instead of rejecting oversized requests.
|
||||
if maxTags := th.App.Config.Tags.MaxTags; maxTags > 0 && limit > maxTags {
|
||||
limit = maxTags
|
||||
}
|
||||
}
|
||||
|
||||
filled := make([]string, 0)
|
||||
|
||||
Reference in new issue
Block a user