Skip to content

Commit eb830b0

Browse files
fix(codeintel): improve upload error hints for auth and permission failures
When upload of a scip index fails, the error message may be misleading or incomplete. The complexity comes from two aspects. First that we have 2 tokens - sourcegraph access token and code host token. This PR makes sure that whenever a token is invalid we get most probable causes. Second part is that even if a token is valid on its own, the user may not have sufficient permission. sourcegraph/sourcegraph#10077 makes the error codes and messages more consistent on the server side. This way user gets a hint if token was wrong or there was a permission problem. To summarise. First of all user gets error message from the server which often is specific (ex. upload failed: must provide gitlab_token). However basing text returned by the server (server does not return structured response like JSON) is IMO fragile so except this error message user gets hints about possible problems. I believe those two together should eliminate confusion and point user in the right direction how to remediate the issue. We also always show the link to the documentation. Additionally existing code was deducing used code host based on the repository URL hostname, here we also check the specified command line parameter. Those hints which are displayed to the user is not a new invention, those were just improved to provide user with more accurate information and to utilise information from the server which effectively is best source of truth what the problem was.
1 parent ad16cda commit eb830b0

File tree

3 files changed

+475
-74
lines changed

3 files changed

+475
-74
lines changed

cmd/src/code_intel_upload.go

Lines changed: 105 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func handleCodeIntelUpload(args []string) error {
7171
}
7272
}
7373
if err != nil {
74-
return handleUploadError(cfg.AccessToken, err)
74+
return err
7575
}
7676

7777
client := cfg.apiClient(codeintelUploadFlags.apiFlags, io.Discard)
@@ -212,94 +212,135 @@ func (e errorWithHint) Error() string {
212212
return fmt.Sprintf("%s\n\n%s\n", e.err, e.hint)
213213
}
214214

215-
// handleUploadError writes the given error to the given output. If the
216-
// given output object is nil then the error will be written to standard out.
217-
//
218-
// This method returns the error that should be passed back up to the runner.
215+
// handleUploadError attaches actionable hints to upload errors and returns
216+
// the enriched error. Only called for actual upload failures (not flag validation).
219217
func handleUploadError(accessToken string, err error) error {
220-
if errors.Is(err, upload.ErrUnauthorized) {
221-
err = attachHintsForAuthorizationError(accessToken, err)
218+
if httpErr := findAuthError(err); httpErr != nil {
219+
isUnauthorized := httpErr.Code == 401
220+
isForbidden := httpErr.Code == 403
221+
222+
displayErr := errors.Newf("upload failed: %s", uploadFailureReason(httpErr))
223+
224+
err = errorWithHint{
225+
err: displayErr,
226+
hint: uploadHints(accessToken, isUnauthorized, isForbidden),
227+
}
222228
}
223229

224230
if codeintelUploadFlags.ignoreUploadFailures {
225-
// Report but don't return the error
226231
fmt.Println(err.Error())
227232
return nil
228233
}
229234

230235
return err
231236
}
232237

233-
func attachHintsForAuthorizationError(accessToken string, originalError error) error {
234-
var actionableHints []string
238+
// findAuthError searches the error chain (including multi-errors from retries)
239+
// for a 401 or 403 ErrUnexpectedStatusCode. Returns nil if none is found.
240+
func findAuthError(err error) *ErrUnexpectedStatusCode {
241+
// Check if it's a multi-error and scan all children.
242+
if multi, ok := err.(errors.MultiError); ok {
243+
for _, e := range multi.Errors() {
244+
if found := findAuthError(e); found != nil {
245+
return found
246+
}
247+
}
248+
return nil
249+
}
250+
251+
var httpErr *ErrUnexpectedStatusCode
252+
if errors.As(err, &httpErr) && (httpErr.Code == 401 || httpErr.Code == 403) {
253+
return httpErr
254+
}
255+
return nil
256+
}
257+
258+
// uploadHints builds hint paragraphs for the Sourcegraph access token,
259+
// code host tokens, and a docs link.
260+
func uploadHints(accessToken string, isUnauthorized, isForbidden bool) string {
261+
var causes []string
235262

236-
likelyTokenError := accessToken == ""
237-
if _, parseErr := accesstoken.ParsePersonalAccessToken(accessToken); accessToken != "" && parseErr != nil {
238-
likelyTokenError = true
239-
actionableHints = append(actionableHints,
240-
"However, the provided access token does not match expected format; was it truncated?",
241-
"Typically the access token looks like sgp_<40 hex chars> or sgp_<instance-id>_<40 hex chars>.")
263+
if h := sourcegraphAccessTokenHint(accessToken, isUnauthorized, isForbidden); h != "" {
264+
causes = append(causes, "- "+h)
242265
}
243266

244-
if likelyTokenError {
245-
return errorWithHint{err: originalError, hint: strings.Join(mergeStringSlices(
246-
[]string{"A Sourcegraph access token must be provided via SRC_ACCESS_TOKEN for uploading SCIP data."},
247-
actionableHints,
248-
[]string{"For more details, see https://sourcegraph.com/docs/cli/how-tos/creating_an_access_token."},
249-
), "\n")}
267+
for _, h := range codeHostTokenHints(isUnauthorized) {
268+
causes = append(causes, "- "+h)
250269
}
251270

252-
needsGitHubToken := strings.HasPrefix(codeintelUploadFlags.repo, "github.com")
253-
needsGitLabToken := strings.HasPrefix(codeintelUploadFlags.repo, "gitlab.com")
271+
var parts []string
272+
parts = append(parts, "Possible causes:\n"+strings.Join(causes, "\n"))
273+
parts = append(parts, "For more details on uploading SCIP indexes, see https://sourcegraph.com/docs/cli/references/code-intel/upload.")
254274

255-
if needsGitHubToken {
256-
if codeintelUploadFlags.gitHubToken != "" {
257-
actionableHints = append(actionableHints,
258-
fmt.Sprintf("The supplied -github-token does not indicate that you have collaborator access to %s.", codeintelUploadFlags.repo),
259-
"Please check the value of the supplied token and its permissions on the code host and try again.",
260-
)
261-
} else {
262-
actionableHints = append(actionableHints,
263-
fmt.Sprintf("Please retry your request with a -github-token=XXX with collaborator access to %s.", codeintelUploadFlags.repo),
264-
"This token will be used to check with the code host that the uploading user has write access to the target repository.",
265-
)
275+
return strings.Join(parts, "\n\n")
276+
}
277+
278+
// sourcegraphAccessTokenHint returns a hint about the Sourcegraph access token
279+
// based on the error type and token state.
280+
func sourcegraphAccessTokenHint(accessToken string, isUnauthorized, isForbidden bool) string {
281+
if isUnauthorized {
282+
if accessToken == "" {
283+
return "No Sourcegraph access token was provided. Set the SRC_ACCESS_TOKEN environment variable to a valid token."
266284
}
267-
} else if needsGitLabToken {
268-
if codeintelUploadFlags.gitLabToken != "" {
269-
actionableHints = append(actionableHints,
270-
fmt.Sprintf("The supplied -gitlab-token does not indicate that you have write access to %s.", codeintelUploadFlags.repo),
271-
"Please check the value of the supplied token and its permissions on the code host and try again.",
272-
)
273-
} else {
274-
actionableHints = append(actionableHints,
275-
fmt.Sprintf("Please retry your request with a -gitlab-token=XXX with write access to %s.", codeintelUploadFlags.repo),
276-
"This token will be used to check with the code host that the uploading user has write access to the target repository.",
277-
)
285+
if _, parseErr := accesstoken.ParsePersonalAccessToken(accessToken); parseErr != nil {
286+
return "The provided Sourcegraph access token does not match the expected format (sgp_<40 hex chars> or sgp_<instance-id>_<40 hex chars>). Was it copied incorrectly or truncated?"
278287
}
279-
} else {
280-
actionableHints = append(actionableHints,
281-
"Verification is supported for the following code hosts: github.com, gitlab.com.",
282-
"Please request support for additional code host verification at https://github.com/sourcegraph/sourcegraph/issues/4967.",
283-
)
288+
return "The Sourcegraph access token may be invalid, expired, or you may be connecting to the wrong Sourcegraph instance."
289+
}
290+
if isForbidden {
291+
return "You may not have sufficient permissions on this Sourcegraph instance."
284292
}
293+
return ""
294+
}
285295

286-
return errorWithHint{err: originalError, hint: strings.Join(mergeStringSlices(
287-
[]string{"This Sourcegraph instance has enforced auth for SCIP uploads."},
288-
actionableHints,
289-
[]string{"For more details, see https://docs.sourcegraph.com/cli/references/code-intel/upload."},
290-
), "\n")}
296+
// codeHostTokenHints returns hints about GitHub or GitLab tokens.
297+
func codeHostTokenHints(isUnauthorized bool) []string {
298+
if codeintelUploadFlags.gitHubToken != "" || strings.HasPrefix(codeintelUploadFlags.repo, "github.com") {
299+
return []string{gitHubTokenHint(isUnauthorized)}
300+
}
301+
if codeintelUploadFlags.gitLabToken != "" || strings.HasPrefix(codeintelUploadFlags.repo, "gitlab.com") {
302+
return []string{gitLabTokenHint(isUnauthorized)}
303+
}
304+
return []string{"Code host verification is supported for github.com and gitlab.com repositories."}
291305
}
292306

293-
// emergencyOutput creates a default Output object writing to standard out.
294-
func emergencyOutput() *output.Output {
295-
return output.NewOutput(os.Stdout, output.OutputOpts{})
307+
// gitHubTokenHint returns a hint about the GitHub token.
308+
// Only called when gitHubToken is set or repo starts with "github.com".
309+
func gitHubTokenHint(isUnauthorized bool) string {
310+
if codeintelUploadFlags.gitHubToken == "" {
311+
return fmt.Sprintf("No -github-token was provided. If this Sourcegraph instance enforces code host authentication, retry with -github-token=<token> for a token with access to %s.", codeintelUploadFlags.repo)
312+
}
313+
if isUnauthorized {
314+
return "The supplied -github-token may be invalid."
315+
}
316+
return "The supplied -github-token may lack the required permissions."
296317
}
297318

298-
func mergeStringSlices(ss ...[]string) []string {
299-
var combined []string
300-
for _, s := range ss {
301-
combined = append(combined, s...)
319+
// gitLabTokenHint returns a hint about the GitLab token.
320+
// Only called when gitLabToken is set or repo starts with "gitlab.com".
321+
func gitLabTokenHint(isUnauthorized bool) string {
322+
if codeintelUploadFlags.gitLabToken == "" {
323+
return fmt.Sprintf("No -gitlab-token was provided. If this Sourcegraph instance enforces code host authentication, retry with -gitlab-token=<token> for a token with access to %s.", codeintelUploadFlags.repo)
324+
}
325+
if isUnauthorized {
326+
return "The supplied -gitlab-token may be invalid."
302327
}
328+
return "The supplied -gitlab-token may lack the required permissions."
329+
}
330+
331+
// uploadFailureReason returns the server's response body if available, or a
332+
// generic reason derived from the HTTP status code.
333+
func uploadFailureReason(httpErr *ErrUnexpectedStatusCode) string {
334+
if httpErr.Body != "" {
335+
return httpErr.Body
336+
}
337+
if httpErr.Code == 401 {
338+
return "unauthorized"
339+
}
340+
return "forbidden"
341+
}
303342

304-
return combined
343+
// emergencyOutput creates a default Output object writing to standard out.
344+
func emergencyOutput() *output.Output {
345+
return output.NewOutput(os.Stdout, output.OutputOpts{})
305346
}

0 commit comments

Comments
 (0)