internal/frontend: return previous fetch result on 404
When a request results in a 404, show the user the previous fetch
result, instead of giving the option to refetch the path. The majority
of the time, clicking "Request <path>" will just show the user when is
in the database.
Change-Id: I652edd86e8ad236498eb78ffc205171309cbbee1
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/281675
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 452a321..6b98ca2 100644
--- a/internal/frontend/404.go
+++ b/internal/frontend/404.go
@@ -5,13 +5,18 @@
package frontend
import (
+ "context"
+ "errors"
"fmt"
+ "html"
"net/http"
"github.com/google/safehtml/template"
+ "github.com/google/safehtml/template/uncheckedconversions"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/log"
+ "golang.org/x/pkgsite/internal/postgres"
"golang.org/x/pkgsite/internal/stdlib"
)
@@ -46,7 +51,27 @@
if stdlib.Contains(fullPath) {
return &serverError{status: http.StatusNotFound}
}
- return pathNotFoundError(fullPath, requestedVersion)
+
+ db, ok := ds.(*postgres.DB)
+ if !ok {
+ return proxydatasourceNotSupportedErr()
+ }
+ fr, err := previousFetchStatusAndResponse(ctx, db, fullPath, requestedVersion)
+ if err != nil || fr.status == http.StatusInternalServerError {
+ if err != nil && !errors.Is(err, derrors.NotFound) {
+ log.Error(ctx, err)
+ }
+ return pathNotFoundError(fullPath, requestedVersion)
+ }
+ return &serverError{
+ status: fr.status,
+ epage: &errorPage{
+ messageTemplate: uncheckedconversions.TrustedTemplateFromStringKnownToSatisfyTypeContract(`
+ <h3 class="Error-message">{{.StatusText}}</h3>
+ <p class="Error-message">` + html.UnescapeString(fr.responseText) + `</p>`),
+ MessageData: struct{ StatusText string }{http.StatusText(fr.status)},
+ },
+ }
}
// pathNotFoundError returns a page with an option on how to
@@ -70,3 +95,51 @@
},
}
}
+
+// previousFetchStatusAndResponse returns the status and response text from a
+// previous fetch of the fullPath and requestedVersion.
+func previousFetchStatusAndResponse(ctx context.Context, db *postgres.DB, fullPath, requestedVersion string) (_ *fetchResult, err error) {
+ defer derrors.Wrap(&err, "fetchRedirectPath(w, r, %q, %q)", fullPath, requestedVersion)
+
+ vm, err := db.GetVersionMap(ctx, fullPath, requestedVersion)
+ if err != nil {
+ return nil, err
+ }
+ if vm.Status != http.StatusNotFound {
+ return resultFromFetchRequest([]*fetchResult{
+ {
+ modulePath: vm.ModulePath,
+ goModPath: vm.GoModPath,
+ status: vm.Status,
+ err: errors.New(vm.Error),
+ },
+ }, fullPath, requestedVersion)
+ }
+
+ // If the status is 404, it likely means that the fullPath is not a
+ // modulePath. Check all of the candidate module paths for the past result.
+ paths, err := candidateModulePaths(fullPath)
+ if err != nil {
+ return nil, err
+ }
+ vms, err := db.GetVersionMapsNon2xxStatus(ctx, paths, requestedVersion)
+ if err != nil {
+ return nil, err
+ }
+ if len(vms) == 0 {
+ return nil, nil
+ }
+ var fetchResults []*fetchResult
+ for _, vm := range vms {
+ fetchResults = append(fetchResults, &fetchResult{
+ modulePath: vm.ModulePath,
+ goModPath: vm.GoModPath,
+ status: vm.Status,
+ err: errors.New(vm.Error),
+ })
+ }
+ if len(fetchResults) == 0 {
+ return nil, derrors.NotFound
+ }
+ return resultFromFetchRequest(fetchResults, fullPath, requestedVersion)
+}
diff --git a/internal/frontend/404_test.go b/internal/frontend/404_test.go
new file mode 100644
index 0000000..5a6d2d7
--- /dev/null
+++ b/internal/frontend/404_test.go
@@ -0,0 +1,93 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package frontend
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/derrors"
+ "golang.org/x/pkgsite/internal/testing/sample"
+)
+
+func TestPreviousFetchStatusAndResponse(t *testing.T) {
+ ctx := context.Background()
+
+ for _, mod := range []struct {
+ path string
+ goModPath string
+ status int
+ }{
+ {"mvdan.cc/sh", "", 200},
+ {"mvdan.cc", "", 404},
+ {"400.mod/foo/bar", "", 400},
+ {"400.mod/foo", "", 404},
+ {"400.mod", "", 404},
+ {"github.com/alternative/ok", "github.com/vanity", 491},
+ {"github.com/alternative/ok/path", "", 404},
+ {"github.com/alternative/bad", "vanity", 491},
+ {"bad.mod/foo/bar", "", 490},
+ {"bad.mod/foo", "", 404},
+ {"bad.mod", "", 490},
+ {"500.mod/foo", "", 404},
+ {"500.mod", "", 500},
+ {"reprocess.mod/foo", "", 520},
+ } {
+ goModPath := mod.goModPath
+ if goModPath == "" {
+ goModPath = mod.path
+ }
+ if err := testDB.UpsertVersionMap(ctx, &internal.VersionMap{
+ ModulePath: mod.path,
+ RequestedVersion: internal.LatestVersion,
+ ResolvedVersion: sample.VersionString,
+ Status: mod.status,
+ GoModPath: goModPath,
+ }); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ for _, test := range []struct {
+ name, path string
+ status int
+ }{
+ {"bad request at path root", "400.mod/foo/bar", 404},
+ {"bad request at mod but 404 at path", "400.mod/foo", 404},
+ {"alternative mod", "github.com/alternative/ok", 491},
+ {"alternative mod package path", "github.com/alternative/ok/path", 491},
+ {"alternative mod bad module path", "github.com/alternative/bad", 404},
+ {"bad module at path", "bad.mod/foo/bar", 404},
+ {"bad module at mod but 404 at path", "bad.mod/foo", 404},
+ {"500", "500.mod/foo", 500},
+ {"mod to reprocess", "reprocess.mod/foo", 404},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ fr, err := previousFetchStatusAndResponse(ctx, testDB, test.path, internal.LatestVersion)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fr.status != test.status {
+ t.Errorf("got %v; want %v", fr.status, test.status)
+ }
+ })
+ }
+
+ for _, test := range []struct {
+ name, path string
+ }{
+ {"path never fetched", "github.com/nonexistent"},
+ {"path never fetched, but top level mod fetched", "mvdan.cc/sh/v3"},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ _, err := previousFetchStatusAndResponse(ctx, testDB, test.path, internal.LatestVersion)
+ if !errors.Is(err, derrors.NotFound) {
+ t.Errorf("got %v; want %v", err, derrors.NotFound)
+ }
+ })
+ }
+}
diff --git a/internal/frontend/fetch.go b/internal/frontend/fetch.go
index ce90461..40f1eb8 100644
--- a/internal/frontend/fetch.go
+++ b/internal/frontend/fetch.go
@@ -158,6 +158,9 @@
log.Errorf(ctx, "fetchAndPoll(ctx, ds, q, %q, %q, %q): %v", modulePath, fullPath, requestedVersion, err)
return http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)
}
+ if fr.status == derrors.ToStatus(derrors.AlternativeModule) {
+ fr.status = http.StatusNotFound
+ }
return fr.status, fr.responseText
}
@@ -247,7 +250,7 @@
fr.responseText = "Oops! Something went wrong."
return fr, nil
case derrors.ToStatus(derrors.AlternativeModule):
- if err := module.CheckImportPath(fr.goModPath); err != nil {
+ if err := module.CheckPath(fr.goModPath); err != nil {
fr.status = http.StatusNotFound
fr.responseText = fmt.Sprintf(`%q does not have a valid module path (%q).`, fullPath, fr.goModPath)
return fr, nil
@@ -259,7 +262,6 @@
fr.status = http.StatusInternalServerError
return fr, err
}
- fr.status = http.StatusNotFound
fr.responseText = h.String()
return fr, nil
}
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index 36eb94f..005c5ff 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -19,6 +19,7 @@
"github.com/jba/templatecheck"
"golang.org/x/net/html"
"golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/middleware"
"golang.org/x/pkgsite/internal/postgres"
@@ -1148,6 +1149,16 @@
if err := testDB.InsertModule(ctx, sampleModule); err != nil {
t.Fatal(err)
}
+ alternativeModule := &internal.VersionMap{
+ ModulePath: "module.path/alternative",
+ GoModPath: sample.ModulePath,
+ RequestedVersion: sample.VersionString,
+ ResolvedVersion: sample.VersionString,
+ Status: derrors.ToStatus(derrors.AlternativeModule),
+ }
+ if err := testDB.UpsertVersionMap(ctx, alternativeModule); err != nil {
+ t.Fatal(err)
+ }
_, handler, _ := newTestServer(t, nil)
for _, test := range []struct {
@@ -1156,6 +1167,7 @@
}{
{"not found", "/invalid-page", http.StatusNotFound},
{"bad request", "/gocloud.dev/@latest/blob", http.StatusBadRequest},
+ {"alternative module", "/" + alternativeModule.ModulePath, http.StatusNotFound},
} {
t.Run(test.name, func(t *testing.T) {
w := httptest.NewRecorder()