{"record":{"id":"609170d3b7494a58","repo":"github/github-mcp-server","slug":"installation-token-response-did-not-contain-a-toke","errorCode":null,"errorMessage":"installation token response did not contain a token","messagePattern":"installation token response did not contain a token","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/githubapp/githubapp.go","lineNumber":169,"sourceCode":"\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusCreated {\n\t\tsnippet, readErr := io.ReadAll(io.LimitReader(resp.Body, 512))\n\t\tif readErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"installation token request failed: %s (reading response: %w)\", resp.Status, readErr)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"installation token request failed: %s: %s\", resp.Status, strings.TrimSpace(string(snippet)))\n\t}\n\n\tvar body struct {\n\t\tToken     string    `json:\"token\"`\n\t\tExpiresAt time.Time `json:\"expires_at\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&body); err != nil {\n\t\treturn nil, fmt.Errorf(\"decoding installation token response: %w\", err)\n\t}\n\tif body.Token == \"\" {\n\t\treturn nil, errors.New(\"installation token response did not contain a token\")\n\t}\n\tif body.ExpiresAt.IsZero() {\n\t\treturn nil, errors.New(\"installation token response did not contain an expiry\")\n\t}\n\treturn &oauth2.Token{\n\t\tAccessToken: body.Token,\n\t\tTokenType:   \"token\",\n\t\tExpiry:      body.ExpiresAt.Add(-refreshBuffer),\n\t}, nil\n}\n\n// Provider caches and refreshes GitHub App installation access tokens.\ntype Provider struct {\n\tsource oauth2.TokenSource\n\tlogger *slog.Logger\n\n\tmu        sync.Mutex\n\terrLogged bool","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/internal/githubapp/githubapp.go#L151-L187","documentation":"decodeBlameCursor parses the 'after' pagination parameter of get_file_blame. Cursors are opaque values minted by encodeBlameCursor: base64.RawURLEncoding of \"blame-range:\"+integer offset. This variant fires at the first guard when base64.RawURLEncoding.DecodeString rejects the input, meaning it is not unpadded URL-safe base64 - it contains '+', '/', '=' padding, or non-alphabet characters.","triggerScenarios":"Passing a standard-alphabet base64 cursor (with '+', '/', or trailing '='), a truncated or whitespace-padded cursor, a cursor that survived URL-encoding as literal '%xx', or arbitrary free text in the after field.","commonSituations":"Client re-encodes the returned cursor with std base64 instead of passing it through; JSON layers that escape or trim the value; hand-building cursors instead of echoing the previous page's nextCursor.","solutions":["Echo the cursor exactly as returned in the previous get_file_blame response's pageInfo - never re-encode it","If constructing one yourself: base64.RawURLEncoding.EncodeToString([]byte(\"blame-range:100\"))","Send after as the plain JSON string value with no URL encoding or trimming","If pagination state is corrupted, restart from after:\"\" and re-page"],"exampleFix":"// before: standard base64 with '=' padding is rejected by RawURLEncoding\n{\"owner\":\"octocat\",\"repo\":\"Hello-World\",\"path\":\"README.md\",\"after\":\"YmxhbWUtcmFuZ2U6MTAw=\"}\n\n// after: unpadded base64url, exactly as returned by the previous response\n{\"owner\":\"octocat\",\"repo\":\"Hello-World\",\"path\":\"README.md\",\"after\":\"YmxhbWUtcmFuZ2U6MTAw\"}","handlingStrategy":"validation","validationCode":"// Validate the 'after' cursor client-side before calling get_file_blame.\nfunc validBlameCursorEncoding(s string) bool {\n\tif s == \"\" {\n\t\treturn true // first page\n\t}\n\tif _, err := base64.RawURLEncoding.DecodeString(s); err != nil {\n\t\treturn false // not unpadded URL-safe base64\n\t}\n\treturn true\n}","typeGuard":"func isInvalidCursorError(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"after cursor is invalid\")\n}","tryCatchPattern":"res, err := callGetFileBlame(ctx, args)\nif isInvalidCursorError(err) {\n\t// cursor state is corrupt: restart pagination from the first page\n\targs[\"after\"] = \"\"\n\tres, err = callGetFileBlame(ctx, args)\n}","preventionTips":["Treat cursors as opaque: store and echo the returned value verbatim","Never re-encode cursors with standard base64 (padding/+// break Raw URL encoding)","Pass after as a plain JSON string without URL encoding","Use after:\"\" for the first page instead of a fabricated value"],"tags":["go","pagination","base64","validation"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}