From 8e2020a48c199031ce8e3d362f2ca5dcdf5abb30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:13:01 +0000 Subject: [PATCH 1/9] Initial plan From ff80277cddf2a10b102c5f9944a85ffc5ac01e67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:25:44 +0000 Subject: [PATCH 2/9] Add Link header with xregistry-root for all API responses Co-authored-by: duglin <1944671+duglin@users.noreply.github.com> --- registry/httpStuff.go | 2 ++ registry/info.go | 2 ++ tests/link_header_test.go | 63 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 tests/link_header_test.go diff --git a/registry/httpStuff.go b/registry/httpStuff.go index b7ac1db..4cb736f 100644 --- a/registry/httpStuff.go +++ b/registry/httpStuff.go @@ -3371,6 +3371,8 @@ func HTTPWriteError(info *RequestInfo, errAny any) { info.StatusCode = xErr.Code info.AddHeader("Content-Type", "application/json; charset=utf-8") + // Ensure Link header is set for errors too + info.AddHeader("Link", fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL)) for k, v := range xErr.Headers { info.AddHeader(k, v) diff --git a/registry/info.go b/registry/info.go index c313c4e..404ab36 100644 --- a/registry/info.go +++ b/registry/info.go @@ -249,6 +249,8 @@ func ParseRequest(tx *Tx, w http.ResponseWriter, r *http.Request) (*RequestInfo, w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add("Access-Control-Allow-Methods", "GET, PATCH, POST, PUT, DELETE") + // Add Link header for xregistry-root as per spec + w.Header().Add("Link", fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL)) if log.GetVerbose() > 2 { defer func() { log.VPrintf(3, "Info:\n%s\n", ToJSON(info)) }() diff --git a/tests/link_header_test.go b/tests/link_header_test.go new file mode 100644 index 0000000..37b22c0 --- /dev/null +++ b/tests/link_header_test.go @@ -0,0 +1,63 @@ +package tests + +import ( + "testing" + + "github.com/xregistry/server/registry" +) + +func TestLinkHeader(t *testing.T) { + reg := NewRegistry("TestLinkHeader") + defer PassDeleteReg(t, reg) + + // Test Link header on registry root GET + XCheckHTTP(t, reg, &HTTPTest{ + Name: "Link header on registry root", + URL: "/", + Method: "GET", + ReqHeaders: []string{}, + ReqBody: "", + + Code: 200, + ResHeaders: []string{"Link:;rel=xregistry-root"}, + ResBody: "*", + }) + + // Test Link header on error response + XCheckHTTP(t, reg, &HTTPTest{ + Name: "Link header on error", + URL: "/notfound", + Method: "GET", + ReqHeaders: []string{}, + ReqBody: "", + + Code: 404, + ResHeaders: []string{"Link:;rel=xregistry-root"}, + ResBody: "*", + }) +} + +func TestLinkHeaderMultiRegistry(t *testing.T) { + reg := NewRegistry("TestLinkHeaderReg1") + defer PassDeleteReg(t, reg) + + reg2, _ := registry.NewRegistry(nil, "TestLinkHeaderReg2") + reg2.SaveAllAndCommit() + defer func() { + reg2.Delete() + reg2.SaveAllAndCommit() + }() + + // Test Link header with multi-registry path (reg- prefix) + XCheckHTTP(t, reg2, &HTTPTest{ + Name: "Link header with reg- prefix", + URL: "/reg-TestLinkHeaderReg2", + Method: "GET", + ReqHeaders: []string{}, + ReqBody: "", + + Code: 200, + ResHeaders: []string{"Link:;rel=xregistry-root"}, + ResBody: "*", + }) +} From b038897b2dddb1b4b9dde226fa629d6a064e665a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:41:49 +0000 Subject: [PATCH 3/9] Move Link header test to http3_test.go and rename to TestHTTPLinkHeader Co-authored-by: duglin <1944671+duglin@users.noreply.github.com> --- tests/http3_test.go | 31 +++++++++++++++++++ tests/link_header_test.go | 63 --------------------------------------- 2 files changed, 31 insertions(+), 63 deletions(-) delete mode 100644 tests/link_header_test.go diff --git a/tests/http3_test.go b/tests/http3_test.go index f0af0a5..3e04e78 100644 --- a/tests/http3_test.go +++ b/tests/http3_test.go @@ -2776,3 +2776,34 @@ func TestHTTPVersWithResLevel(t *testing.T) { } `) } + +func TestHTTPLinkHeader(t *testing.T) { + reg := NewRegistry("TestHTTPLinkHeader") + defer PassDeleteReg(t, reg) + + // Test Link header on registry root GET + XCheckHTTP(t, reg, &HTTPTest{ + Name: "Link header on registry root", + URL: "/", + Method: "GET", + ReqHeaders: []string{}, + ReqBody: "", + + Code: 200, + ResHeaders: []string{"Link:;rel=xregistry-root"}, + ResBody: "*", + }) + + // Test Link header on error response + XCheckHTTP(t, reg, &HTTPTest{ + Name: "Link header on error", + URL: "/notfound", + Method: "GET", + ReqHeaders: []string{}, + ReqBody: "", + + Code: 404, + ResHeaders: []string{"Link:;rel=xregistry-root"}, + ResBody: "*", + }) +} diff --git a/tests/link_header_test.go b/tests/link_header_test.go deleted file mode 100644 index 37b22c0..0000000 --- a/tests/link_header_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package tests - -import ( - "testing" - - "github.com/xregistry/server/registry" -) - -func TestLinkHeader(t *testing.T) { - reg := NewRegistry("TestLinkHeader") - defer PassDeleteReg(t, reg) - - // Test Link header on registry root GET - XCheckHTTP(t, reg, &HTTPTest{ - Name: "Link header on registry root", - URL: "/", - Method: "GET", - ReqHeaders: []string{}, - ReqBody: "", - - Code: 200, - ResHeaders: []string{"Link:;rel=xregistry-root"}, - ResBody: "*", - }) - - // Test Link header on error response - XCheckHTTP(t, reg, &HTTPTest{ - Name: "Link header on error", - URL: "/notfound", - Method: "GET", - ReqHeaders: []string{}, - ReqBody: "", - - Code: 404, - ResHeaders: []string{"Link:;rel=xregistry-root"}, - ResBody: "*", - }) -} - -func TestLinkHeaderMultiRegistry(t *testing.T) { - reg := NewRegistry("TestLinkHeaderReg1") - defer PassDeleteReg(t, reg) - - reg2, _ := registry.NewRegistry(nil, "TestLinkHeaderReg2") - reg2.SaveAllAndCommit() - defer func() { - reg2.Delete() - reg2.SaveAllAndCommit() - }() - - // Test Link header with multi-registry path (reg- prefix) - XCheckHTTP(t, reg2, &HTTPTest{ - Name: "Link header with reg- prefix", - URL: "/reg-TestLinkHeaderReg2", - Method: "GET", - ReqHeaders: []string{}, - ReqBody: "", - - Code: 200, - ResHeaders: []string{"Link:;rel=xregistry-root"}, - ResBody: "*", - }) -} From 2745554bf6ae7c4fcfbc6b8a292ebd31bd76bdc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:57:53 +0000 Subject: [PATCH 4/9] Add tests for non-default registry, all HTTP methods, and duplicate header detection Co-authored-by: duglin <1944671+duglin@users.noreply.github.com> --- registry/httpStuff.go | 2 ++ tests/http3_test.go | 22 ++++++++++++++++++++++ tests/misc_test.go | 12 ++++++++++++ 3 files changed, 36 insertions(+) diff --git a/registry/httpStuff.go b/registry/httpStuff.go index 4cb736f..558324f 100644 --- a/registry/httpStuff.go +++ b/registry/httpStuff.go @@ -3355,6 +3355,8 @@ func HTTPProxy(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add("Access-Control-Allow-Methods", "GET, PATCH, POST, PUT, DELETE") + // Add Link header pointing to the local server root + w.Header().Add("Link", fmt.Sprintf(";rel=xregistry-root", r.Host)) html := GenerateUI(info, data) w.Write(html) diff --git a/tests/http3_test.go b/tests/http3_test.go index 3e04e78..589712f 100644 --- a/tests/http3_test.go +++ b/tests/http3_test.go @@ -2806,4 +2806,26 @@ func TestHTTPLinkHeader(t *testing.T) { ResHeaders: []string{"Link:;rel=xregistry-root"}, ResBody: "*", }) + + // Test Link header with non-default registry (reg- prefix) + XCheckHTTP(t, reg, &HTTPTest{ + Name: "Link header with reg- prefix", + URL: "/reg-TestHTTPLinkHeader", + Method: "GET", + ReqHeaders: []string{}, + ReqBody: "", + + Code: 200, + ResHeaders: []string{"Link:;rel=xregistry-root"}, + ResBody: "*", + }) + + // Test to ensure Link header doesn't appear twice in error cases + res := XDoHTTP(t, reg, "GET", "/notfound", "") + linkHeaders := res.Header.Values("Link") + XCheck(t, len(linkHeaders) == 1, + "Link header should appear exactly once, got %d occurrences: %v", + len(linkHeaders), linkHeaders) + XEqual(t, "Link header value", + linkHeaders[0], ";rel=xregistry-root") } diff --git a/tests/misc_test.go b/tests/misc_test.go index e861095..f0621f5 100644 --- a/tests/misc_test.go +++ b/tests/misc_test.go @@ -149,6 +149,18 @@ func TestMiscCORS(t *testing.T) { XEqual(t, "cors header", res.Header.Get("Access-Control-Allow-Methods"), "GET, PATCH, POST, PUT, DELETE") + + // Check Link header is present + linkHeader := res.Header.Get("Link") + XCheck(t, linkHeader != "", "Link header should be present for %s %s", test.method, test.url) + + // Verify Link header has correct format with registry root + expectedURL := "http://localhost:8181" + if test.url == "/reg-TestMiscCORS" { + expectedURL = "http://localhost:8181/reg-TestMiscCORS" + } + XEqual(t, "link header", + linkHeader, fmt.Sprintf("<%s>;rel=xregistry-root", expectedURL)) } } From c13ef095b3c2ecc015091fcad2becd4691056a30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:11:47 +0000 Subject: [PATCH 5/9] Remove verbose comments from Link header implementation Co-authored-by: duglin <1944671+duglin@users.noreply.github.com> --- registry/httpStuff.go | 2 -- registry/info.go | 1 - tests/misc_test.go | 2 -- 3 files changed, 5 deletions(-) diff --git a/registry/httpStuff.go b/registry/httpStuff.go index 558324f..0e7c2b3 100644 --- a/registry/httpStuff.go +++ b/registry/httpStuff.go @@ -3355,7 +3355,6 @@ func HTTPProxy(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add("Access-Control-Allow-Methods", "GET, PATCH, POST, PUT, DELETE") - // Add Link header pointing to the local server root w.Header().Add("Link", fmt.Sprintf(";rel=xregistry-root", r.Host)) html := GenerateUI(info, data) @@ -3373,7 +3372,6 @@ func HTTPWriteError(info *RequestInfo, errAny any) { info.StatusCode = xErr.Code info.AddHeader("Content-Type", "application/json; charset=utf-8") - // Ensure Link header is set for errors too info.AddHeader("Link", fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL)) for k, v := range xErr.Headers { diff --git a/registry/info.go b/registry/info.go index 404ab36..da22693 100644 --- a/registry/info.go +++ b/registry/info.go @@ -249,7 +249,6 @@ func ParseRequest(tx *Tx, w http.ResponseWriter, r *http.Request) (*RequestInfo, w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add("Access-Control-Allow-Methods", "GET, PATCH, POST, PUT, DELETE") - // Add Link header for xregistry-root as per spec w.Header().Add("Link", fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL)) if log.GetVerbose() > 2 { diff --git a/tests/misc_test.go b/tests/misc_test.go index f0621f5..abede4a 100644 --- a/tests/misc_test.go +++ b/tests/misc_test.go @@ -150,11 +150,9 @@ func TestMiscCORS(t *testing.T) { res.Header.Get("Access-Control-Allow-Methods"), "GET, PATCH, POST, PUT, DELETE") - // Check Link header is present linkHeader := res.Header.Get("Link") XCheck(t, linkHeader != "", "Link header should be present for %s %s", test.method, test.url) - // Verify Link header has correct format with registry root expectedURL := "http://localhost:8181" if test.url == "/reg-TestMiscCORS" { expectedURL = "http://localhost:8181/reg-TestMiscCORS" From 63d5f121ab6c23ebfe87b7236ce6da07456076ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:20:35 +0000 Subject: [PATCH 6/9] Remove completed Link header work item from todo file Co-authored-by: duglin <1944671+duglin@users.noreply.github.com> --- todo | 1 - 1 file changed, 1 deletion(-) diff --git a/todo b/todo index 634d388..dc61ff7 100644 --- a/todo +++ b/todo @@ -136,7 +136,6 @@ TODOs: - make model changes via API show up in modelsource - ban model changes that turn on hasDoc if Resources have RESOURCE* attribute extension names -- support Link: ;rel=xregistry-root http header - make sure ?defaultversionid only processed on write ops (bad_flag) - look for more bad_request error that we can specialize - add tests for setting modelsource with $include stuff - and upload data at the same time From 0e997d984085561810ad1aa2c3216e20a862c37b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:18:43 +0000 Subject: [PATCH 7/9] Fix duplicate Link header in error responses Co-authored-by: duglin <1944671+duglin@users.noreply.github.com> --- registry/httpStuff.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/registry/httpStuff.go b/registry/httpStuff.go index 84a4a5b..307afcd 100644 --- a/registry/httpStuff.go +++ b/registry/httpStuff.go @@ -3457,7 +3457,9 @@ func HTTPWriteError(info *RequestInfo, errAny any) { if info.GetHeader("Content-Type") == "" { info.SetHeader("Content-Type", "application/json; charset=utf-8") } - info.AddHeader("Link", fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL)) + if info.GetHeader("Link") == "" { + info.AddHeader("Link", fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL)) + } for k, v := range xErr.Headers { info.AddHeader(k, v) From 9d9b2403a48b70573ed724bff1d0d8f0a1830094 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:35:31 +0000 Subject: [PATCH 8/9] Check for specific xregistry-root Link header to allow multiple Link headers Co-authored-by: duglin <1944671+duglin@users.noreply.github.com> --- registry/httpStuff.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/registry/httpStuff.go b/registry/httpStuff.go index 307afcd..bc73e35 100644 --- a/registry/httpStuff.go +++ b/registry/httpStuff.go @@ -3457,8 +3457,18 @@ func HTTPWriteError(info *RequestInfo, errAny any) { if info.GetHeader("Content-Type") == "" { info.SetHeader("Content-Type", "application/json; charset=utf-8") } - if info.GetHeader("Link") == "" { - info.AddHeader("Link", fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL)) + // Add Link header with xregistry-root rel if not already present + // Check all Link header values to avoid duplicating the xregistry-root link + linkValue := fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL) + hasXRegistryLink := false + for _, v := range info.GetHeaderValues("Link") { + if v == linkValue { + hasXRegistryLink = true + break + } + } + if !hasXRegistryLink { + info.AddHeader("Link", linkValue) } for k, v := range xErr.Headers { From d340d0bcc1f65cabc203b97c89383a93afb93cd8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:50:46 +0000 Subject: [PATCH 9/9] Parse Link header rel value instead of exact string matching Co-authored-by: duglin <1944671+duglin@users.noreply.github.com> --- registry/httpStuff.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/registry/httpStuff.go b/registry/httpStuff.go index bc73e35..8f5d726 100644 --- a/registry/httpStuff.go +++ b/registry/httpStuff.go @@ -3457,17 +3457,28 @@ func HTTPWriteError(info *RequestInfo, errAny any) { if info.GetHeader("Content-Type") == "" { info.SetHeader("Content-Type", "application/json; charset=utf-8") } - // Add Link header with xregistry-root rel if not already present - // Check all Link header values to avoid duplicating the xregistry-root link + // Add or replace Link header with xregistry-root rel + // Check if there's already a Link header with rel=xregistry-root linkValue := fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL) + existingLinks := info.GetHeaderValues("Link") hasXRegistryLink := false - for _, v := range info.GetHeaderValues("Link") { - if v == linkValue { + for i, v := range existingLinks { + // Check if this Link header has rel=xregistry-root + if strings.Contains(v, "rel=xregistry-root") || strings.Contains(v, "rel=\"xregistry-root\"") { + // Replace it with the current value + existingLinks[i] = linkValue hasXRegistryLink = true break } } - if !hasXRegistryLink { + if hasXRegistryLink { + // Clear all Link headers and re-add them with the updated value + info.OriginalResponse.Header().Del("Link") + for _, v := range existingLinks { + info.AddHeader("Link", v) + } + } else { + // No existing xregistry-root link, just add it info.AddHeader("Link", linkValue) }