Repository: ipfs/boxo (language: go)

Issue:
Gateway: `handleSuperfluousNamespace` doesn't work with Hostnames
When the gateway is wrapped with the Hostname Handler, the superfluous namespace redirect doesn't work. This is because HostnameHandler will immediately say that the path is invalid. I suspect this may be happening with other things. It has probably happened before, but we've never noticed.

Solutions:

1. Remove it. Do we really need it?
2. Add it to the hostname handler. Then, we'll be verifying this twice which contains path validation. We already check the URI query twice, but that's cheap: if it doesn't exist, nothing is done. Here we always run the path.IsValid, which costs more.
3. Rework it to make it simpler: just check if URL starts with `/ipfs/ipfs` or `/ipfs/ipns` and do the redirect. Do not do path validation. That will happen after redirection anyways. Then add it to the Hostname handler again.

- [ ] Add conformance test, hint should say it's quality of life improvement.


Discussion / hints:
@lidel right now the handler does a 400 Bad Request with an HTML document that does a redirection using `http-equiv`. What do you think about simplifying this to a Permanent Redirect and streamlining our redirects?

Fix patch:
diff --git a/gateway/handler.go b/gateway/handler.go
index a64d22971..6963bdebf 100644
--- a/gateway/handler.go
+++ b/gateway/handler.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"html/template"
 	"io"
 	"mime"
 	"net/http"
@@ -43,26 +42,6 @@ var (
 	noModtime = time.Unix(0, 0) // disables Last-Modified header if passed as modtime
 )
 
-// HTML-based redirect for errors which can be recovered from, but we want
-// to provide hint to people that they should fix things on their end.
-var redirectTemplate = template.Must(template.New("redirect").Parse(`<!DOCTYPE html>
-<html>
-	<head>
-		<meta charset="utf-8">
-		<meta http-equiv="refresh" content="10;url={{.RedirectURL}}" />
-		<link rel="canonical" href="{{.RedirectURL}}" />
-	</head>
-	<body>
-		<pre>{{.ErrorMsg}}</pre><pre>(if a redirect does not happen in 10 seconds, use "{{.SuggestedPath}}" instead)</pre>
-	</body>
-</html>`))
-
-type redirectTemplateData struct {
-	RedirectURL   string
-	SuggestedPath string
-	ErrorMsg      string
-}
-
 // handler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/<path>)
 // (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link)
 type handler struct {
@@ -903,21 +882,8 @@ func (i *handler) handleSuperfluousNamespace(w http.ResponseWriter, r *http.Requ
 		q, _ := url.ParseQuery(r.URL.RawQuery)
 		intendedURL = intendedURL + "?" + q.Encode()
 	}
-	// return HTTP 400 (Bad Request) with HTML error page that:
-	// - points at correct canonical path via <link> header
-	// - displays human-readable error
-	// - redirects to intendedURL after a short delay
-
-	w.WriteHeader(http.StatusBadRequest)
-	err = redirectTemplate.Execute(w, redirectTemplateData{
-		RedirectURL:   intendedURL,
-		SuggestedPath: intendedPath.String(),
-		ErrorMsg:      fmt.Sprintf("invalid path: %q should be %q", r.URL.Path, intendedPath.String()),
-	})
-	if err != nil {
-		_, _ = w.Write([]byte(fmt.Sprintf("error during body generation: %v", err)))
-	}
 
+	http.Redirect(w, r, intendedURL, http.StatusMovedPermanently)
 	return true
 }
 


Test patch:
diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go
index 4785ec759..031a184a5 100644
--- a/gateway/gateway_test.go
+++ b/gateway/gateway_test.go
@@ -577,6 +577,92 @@ func TestRedirects(t *testing.T) {
 		do(http.MethodGet)
 		do(http.MethodHead)
 	})
+
+	t.Run("Superfluous namespace", func(t *testing.T) {
+		t.Parallel()
+
+		backend, root := newMockBackend(t, "fixtures.car")
+		backend.namesys["/ipns/dnslink-gateway.com"] = newMockNamesysItem(path.FromCid(root), 0)
+		backend.namesys["/ipns/dnslink-website.com"] = newMockNamesysItem(path.FromCid(root), 0)
+
+		ts := newTestServerWithConfig(t, backend, Config{
+			NoDNSLink: false,
+			PublicGateways: map[string]*PublicGateway{
+				"dnslink-gateway.com": {
+					Paths:                 []string{"/ipfs", "/ipns"},
+					NoDNSLink:             false,
+					DeserializedResponses: true,
+				},
+				"dnslink-website.com": {
+					Paths:                 []string{},
+					NoDNSLink:             false,
+					DeserializedResponses: true,
+				},
+				"gateway.com": {
+					Paths:                 []string{"/ipfs"},
+					UseSubdomains:         false,
+					NoDNSLink:             true,
+					DeserializedResponses: true,
+				},
+				"subdomain-gateway.com": {
+					Paths:                 []string{"/ipfs", "/ipns"},
+					UseSubdomains:         true,
+					NoDNSLink:             true,
+					DeserializedResponses: true,
+				},
+			},
+			DeserializedResponses: true,
+		})
+
+		for _, test := range []struct {
+			host     string
+			path     string
+			status   int
+			location string
+		}{
+			// Barebones gateway
+			{"", "/ipfs/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusMovedPermanently, "/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"},
+			{"", "/ipfs/ipns/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusMovedPermanently, "/ipns/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"},
+			{"", "/ipfs/ipns/dnslink.com", http.StatusMovedPermanently, "/ipns/dnslink.com"},
+
+			// DNSLink Gateway with /ipfs and /ipns enabled
+			{"dnslink-gateway.com", "/ipfs/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusMovedPermanently, "/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"},
+			{"dnslink-gateway.com", "/ipfs/ipns/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusMovedPermanently, "/ipns/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"},
+			{"dnslink-gateway.com", "/ipfs/ipns/dnslink.com", http.StatusMovedPermanently, "/ipns/dnslink.com"},
+
+			// DNSLink Gateway without /ipfs and /ipns
+			{"dnslink-website.com", "/ipfs/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusNotFound, ""},
+			{"dnslink-website.com", "/ipfs/ipns/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusNotFound, ""},
+			{"dnslink-website.com", "/ipfs/ipns/dnslink.com", http.StatusNotFound, ""},
+
+			// Public gateway
+			{"gateway.com", "/ipfs/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusMovedPermanently, "/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"},
+			{"gateway.com", "/ipfs/ipns/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusMovedPermanently, "/ipns/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"},
+			{"gateway.com", "/ipfs/ipns/dnslink.com", http.StatusMovedPermanently, "/ipns/dnslink.com"},
+
+			// Subdomain gateway
+			{"subdomain-gateway.com", "/ipfs/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusMovedPermanently, "/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"},
+			{"subdomain-gateway.com", "/ipfs/ipns/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", http.StatusMovedPermanently, "/ipns/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"},
+			{"subdomain-gateway.com", "/ipfs/ipns/dnslink.com", http.StatusMovedPermanently, "/ipns/dnslink.com"},
+		} {
+			testName := ts.URL + test.path
+			if test.host != "" {
+				testName += " " + test.host
+			}
+
+			t.Run(testName, func(t *testing.T) {
+				req := mustNewRequest(t, http.MethodGet, ts.URL+test.path, nil)
+				req.Header.Set("Accept", "text/html")
+				if test.host != "" {
+					req.Host = test.host
+				}
+				resp := mustDoWithoutRedirect(t, req)
+				defer resp.Body.Close()
+				require.Equal(t, test.status, resp.StatusCode)
+				require.Equal(t, test.location, resp.Header.Get("Location"))
+			})
+		}
+	})
 }
 
 func TestDeserializedResponses(t *testing.T) {