internal/frontend: refactor resultFromFetchRequest

fetchRequestStatusAndResponseText is renamed to resultFromFetchRequest,
and now returns a fetchResult and error.

This refactor will allow us to use this function on a 404 in the next CL
in order to determine if the result of a previous fetch can be shown.

When a previous fetch request results in a 500, allow a refetch of that
path when the taskIDChangeInterval has exceeded.

Change-Id: I978eeccaa2f58c9e640f25b8773b4ecfab7e9717
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/281676
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/fetch.go b/internal/frontend/fetch.go
index 54bd960..ce90461 100644
--- a/internal/frontend/fetch.go
+++ b/internal/frontend/fetch.go
@@ -119,10 +119,11 @@
 }
 
 type fetchResult struct {
-	modulePath string
-	goModPath  string
-	status     int
-	err        error
+	modulePath   string
+	goModPath    string
+	status       int
+	err          error
+	responseText string
 }
 
 func (s *Server) fetchAndPoll(ctx context.Context, ds internal.DataSource, modulePath, fullPath, requestedVersion string) (status int, responseText string) {
@@ -152,7 +153,12 @@
 		return http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)
 	}
 	results := s.checkPossibleModulePaths(ctx, db, fullPath, requestedVersion, modulePaths, true)
-	return fetchRequestStatusAndResponseText(results, fullPath, requestedVersion)
+	fr, err := resultFromFetchRequest(results, fullPath, requestedVersion)
+	if err != nil {
+		log.Errorf(ctx, "fetchAndPoll(ctx, ds, q, %q, %q, %q): %v", modulePath, fullPath, requestedVersion, err)
+		return http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)
+	}
+	return fr.status, fr.responseText
 }
 
 // checkPossibleModulePaths checks all modulePaths at the requestedVersion, to see
@@ -209,7 +215,7 @@
 	return results
 }
 
-// fetchRequestStatusAndResponseText returns the HTTP status code and response
+// resultFromFetchRequest returns the HTTP status code and response
 // text from the results of fetching possible module paths for fullPath at the
 // requestedVersion. It is assumed the results are sorted in order of
 // decreasing modulePath length, so the first result that is not a
@@ -217,7 +223,12 @@
 // path was found that shares the path prefix of fullPath, the responseText will
 // contain that information. The status and responseText will be displayed to the
 // user.
-func fetchRequestStatusAndResponseText(results []*fetchResult, fullPath, requestedVersion string) (int, string) {
+func resultFromFetchRequest(results []*fetchResult, fullPath, requestedVersion string) (_ *fetchResult, err error) {
+	defer derrors.Wrap(&err, "resultFromFetchRequest(results, %q, %q)", fullPath, requestedVersion)
+	if len(results) == 0 {
+		return nil, fmt.Errorf("no results")
+	}
+
 	var moduleMatchingPathPrefix string
 	for _, fr := range results {
 		switch fr.status {
@@ -225,25 +236,32 @@
 		// appropriate result is found, return. Otherwise, look at the next
 		// path.
 		case http.StatusOK:
-			return fr.status, ""
+			return fr, nil
 		case http.StatusRequestTimeout:
 			// If the context timed out or was canceled before all of the requests
 			// finished, return an error letting the user to check back later. The
 			// worker will still be processing the modules in the background.
-			return fr.status, fmt.Sprintf("We're still working on “%s”. Check back in a few minutes!", displayPath(fullPath, requestedVersion))
+			fr.responseText = fmt.Sprintf("We're still working on “%s”. Check back in a few minutes!", displayPath(fullPath, requestedVersion))
+			return fr, nil
 		case http.StatusInternalServerError:
-			return fr.status, "Oops! Something went wrong."
+			fr.responseText = "Oops! Something went wrong."
+			return fr, nil
 		case derrors.ToStatus(derrors.AlternativeModule):
 			if err := module.CheckImportPath(fr.goModPath); err != nil {
-				return http.StatusNotFound, fmt.Sprintf(`%q does not have a valid module path (%q).`, fullPath, fr.goModPath)
+				fr.status = http.StatusNotFound
+				fr.responseText = fmt.Sprintf(`%q does not have a valid module path (%q).`, fullPath, fr.goModPath)
+				return fr, nil
 			}
 			t := template.Must(template.New("").Parse(`{{.}}`))
 			h, err := t.ExecuteToHTML(fmt.Sprintf("%s is not a valid path. Were you looking for “<a href='https://pkg.go.dev/%s'>%s</a>”?",
 				displayPath(fullPath, requestedVersion), fr.goModPath, fr.goModPath))
 			if err != nil {
-				return http.StatusInternalServerError, err.Error()
+				fr.status = http.StatusInternalServerError
+				return fr, err
 			}
-			return http.StatusNotFound, h.String()
+			fr.status = http.StatusNotFound
+			fr.responseText = h.String()
+			return fr, nil
 		}
 
 		// A module was found for a prefix of the path, but the path did not exist
@@ -256,6 +274,8 @@
 			moduleMatchingPathPrefix = fr.modulePath
 		}
 	}
+
+	fr := results[0]
 	if moduleMatchingPathPrefix != "" {
 		t := template.Must(template.New("").Parse(`{{.}}`))
 		h, err := t.ExecuteToHTML(fmt.Sprintf("Package %s could not be found, but you can view module “%s” at <a href='https://pkg.go.dev/%s'>pkg.go.dev/%s</a>.",
@@ -265,15 +285,20 @@
 			displayPath(moduleMatchingPathPrefix, requestedVersion),
 		))
 		if err != nil {
-			return http.StatusInternalServerError, err.Error()
+			fr.status = http.StatusInternalServerError
+			return fr, err
 		}
-		return http.StatusNotFound, h.String()
+		fr.status = http.StatusNotFound
+		fr.responseText = h.String()
+		return fr, nil
 	}
 	p := fullPath
 	if requestedVersion != internal.LatestVersion {
 		p = fullPath + "@" + requestedVersion
 	}
-	return http.StatusNotFound, fmt.Sprintf("%q could not be found.", p)
+	fr.status = http.StatusNotFound
+	fr.responseText = fmt.Sprintf("%q could not be found.", p)
+	return fr, nil
 }
 
 func displayPath(path, version string) string {
@@ -361,11 +386,9 @@
 	}
 
 	switch fr.status {
-	case http.StatusInternalServerError:
-		fr.err = fmt.Errorf("%q: %v", http.StatusText(fr.status), vm.Error)
-		return fr
 	case http.StatusNotFound,
-		derrors.ToStatus(derrors.DBModuleInsertInvalid):
+		derrors.ToStatus(derrors.DBModuleInsertInvalid),
+		http.StatusInternalServerError:
 		if time.Since(vm.UpdatedAt) > taskIDChangeInterval {
 			// If the duration of taskIDChangeInterval has passed since
 			// a module_path was last inserted into version_map with a failed status,
@@ -383,8 +406,12 @@
 			fr.status = statusNotFoundInVersionMap
 			return fr
 		}
-		// The version_map indicates that the proxy returned a 404/410.
-		fr.err = errModuleDoesNotExist
+		if fr.status == http.StatusInternalServerError {
+			fr.err = fmt.Errorf("%q: %v", http.StatusText(fr.status), vm.Error)
+		} else {
+			// The version_map indicates that the proxy returned a 404/410.
+			fr.err = errModuleDoesNotExist
+		}
 		return fr
 	case derrors.ToStatus(derrors.AlternativeModule):
 		// The row indicates that the provided module path did not match the