Skip to content
Draft
25 changes: 25 additions & 0 deletions registry/httpStuff.go
Original file line number Diff line number Diff line change
Expand Up @@ -3436,6 +3436,7 @@ 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")
w.Header().Add("Link", fmt.Sprintf("<http://%s>;rel=xregistry-root", r.Host))

html := GenerateUI(info, data)
w.Write(html)
Expand All @@ -3456,6 +3457,30 @@ func HTTPWriteError(info *RequestInfo, errAny any) {
if info.GetHeader("Content-Type") == "" {
info.SetHeader("Content-Type", "application/json; charset=utf-8")
}
// 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 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 {
// 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)
}

for k, v := range xErr.Headers {
info.AddHeader(k, v)
Expand Down
1 change: 1 addition & 0 deletions registry/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ 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")
w.Header().Add("Link", fmt.Sprintf("<%s>;rel=xregistry-root", info.BaseURL))

if log.GetVerbose() > 2 {
defer func() { log.Printf("Info:\n%s\n", ToJSON(info)) }()
Expand Down
53 changes: 53 additions & 0 deletions tests/http3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2779,6 +2779,59 @@ 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:<http://localhost:8181>;rel=xregistry-root"},
ResBody: "*",
})

// Test Link header on error response
XCheckHTTP(t, reg, &HTTPTest{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a test that doesn't use the default registry (so URL is /reg-TestHTTPLinkHeader) and let's check for the Link header in TestMiscCORS test too, that way we're testing all http methods, not just GET.

I'm worried that the Link header might appear twice in error cases - can we add a test to make sure that doesn't happen?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added all requested tests in commit 2745554:

  1. Test with non-default registry using /reg-TestHTTPLinkHeader URL
  2. Link header verification in TestMiscCORS for all HTTP methods (GET, POST, PUT, PATCH, DELETE)
  3. Duplicate header detection test that verifies Link header appears exactly once in error cases
  4. Added Link header to HTTPProxy function for proxy responses

Name: "Link header on error",
URL: "/notfound",
Method: "GET",
ReqHeaders: []string{},
ReqBody: "",

Code: 404,
ResHeaders: []string{"Link:<http://localhost:8181>;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:<http://localhost:8181/reg-TestHTTPLinkHeader>;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], "<http://localhost:8181>;rel=xregistry-root")
}

func TestHTTPIgnore(t *testing.T) {
reg := NewRegistry("TestHTTPIgnore")
defer PassDeleteReg(t, reg)
Expand Down
10 changes: 10 additions & 0 deletions tests/misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ func TestMiscCORS(t *testing.T) {
XEqual(t, "cors header",
res.Header.Get("Access-Control-Allow-Methods"),
"GET, PATCH, POST, PUT, DELETE")

linkHeader := res.Header.Get("Link")
XCheck(t, linkHeader != "", "Link header should be present for %s %s", test.method, test.url)

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))
}
}

Expand Down
1 change: 0 additions & 1 deletion todo
Original file line number Diff line number Diff line change
Expand Up @@ -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: <URL-TO-XREGISTRY-ROOT>;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
Expand Down