internal/frontend: fix redirect loops

Fixes golang/go#44534

Change-Id: I5ce5977d65941739174845969fc9215a11ee7218
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/295549
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/frontend/404.go b/internal/frontend/404.go
index 76a34b4..6227084 100644
--- a/internal/frontend/404.go
+++ b/internal/frontend/404.go
@@ -81,10 +81,22 @@
 	}
 	switch fr.status {
 	case http.StatusFound, derrors.ToStatus(derrors.AlternativeModule):
+		if fr.goModPath == fullPath {
+			// The redirectPath and the fullpath are the same. Do not redirect
+			// to avoid ending up in a loop.
+			return errUnitNotFoundWithoutFetch
+		}
+		vm, err := db.GetVersionMap(ctx, fr.goModPath, internal.LatestVersion)
+		if (err != nil && !errors.Is(err, derrors.NotFound)) ||
+			(vm != nil && vm.Status != http.StatusOK) {
+			// We attempted to fetch the canonical module path before and were
+			// not successful. Do not redirect this request.
+			return errUnitNotFoundWithoutFetch
+		}
 		u := constructUnitURL(fr.goModPath, fr.goModPath, internal.LatestVersion)
 		cookie.Set(w, cookie.AlternativeModuleFlash, fullPath, u)
 		http.Redirect(w, r, u, http.StatusFound)
-		return
+		return nil
 	case http.StatusInternalServerError:
 		return pathNotFoundError(fullPath, requestedVersion)
 	default:
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index ff84e5b..277830e 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -1239,6 +1239,62 @@
 	}
 }
 
+func TestServer404Redirect_NoLoop(t *testing.T) {
+	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
+	defer cancel()
+
+	altPath := "module.path/alternative"
+	goModPath := "module.path/alternative/pkg"
+	defer postgres.ResetTestDB(testDB, t)
+	sampleModule := sample.DefaultModule()
+	postgres.MustInsertModule(ctx, t, testDB, sampleModule)
+	alternativeModule := &internal.VersionMap{
+		ModulePath:       altPath,
+		GoModPath:        goModPath,
+		RequestedVersion: internal.LatestVersion,
+		ResolvedVersion:  sample.VersionString,
+		Status:           derrors.ToStatus(derrors.AlternativeModule),
+	}
+	alternativeModulePkg := &internal.VersionMap{
+		ModulePath:       goModPath,
+		GoModPath:        goModPath,
+		RequestedVersion: internal.LatestVersion,
+		ResolvedVersion:  sample.VersionString,
+		Status:           http.StatusNotFound,
+	}
+	if err := testDB.UpsertVersionMap(ctx, alternativeModule); err != nil {
+		t.Fatal(err)
+	}
+	if err := testDB.UpsertVersionMap(ctx, alternativeModulePkg); err != nil {
+		t.Fatal(err)
+	}
+
+	rs, err := miniredis.Run()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer rs.Close()
+
+	_, handler, _ := newTestServer(t, nil, redis.NewClient(&redis.Options{Addr: rs.Addr()}))
+
+	for _, test := range []struct {
+		name, path string
+		status     int
+	}{
+		{"do not redirect if alternative module does not successfully return", "/" + altPath, http.StatusNotFound},
+		{"do not redirect go mod path endlessly", "/" + goModPath, http.StatusNotFound},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			w := httptest.NewRecorder()
+			handler.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
+			// Check for http.StatusFound, which indicates a redirect.
+			if w.Code != test.status {
+				t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.status)
+			}
+		})
+	}
+}
+
 // Verify that some paths that aren't found will redirect to valid pages.
 // Sometimes redirection sets the AlternativeModuleFlash cookie and puts
 // up a banner.