Warn about missing OAuth scopes when reporting HTTP 4xx errors
If a 4xx server response lists scopes in the X-Accepted-Oauth-Scopes
header that are not present in the X-Oauth-Scopes header, the final
error messaging on stderr will now include a hint for the user that they
might need to request the additional scope:
$ gh codespace list
error getting codespaces: HTTP 403: Must have admin rights to Repository. (https://api.github.com/user/codespaces?per_page=30)
This API operation needs the "codespace" scope. To request it, run: gh auth refresh -h github.com -s codespace
This commit is contained in:
parent
9f1a1d8805
commit
2ca18e0600
7 changed files with 165 additions and 58 deletions
|
|
@ -384,12 +384,14 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream
|
|||
}
|
||||
}
|
||||
|
||||
if serverError == "" && resp.StatusCode > 299 {
|
||||
serverError = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
if serverError != "" {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "gh: %s\n", serverError)
|
||||
err = cmdutil.SilentError
|
||||
return
|
||||
} else if resp.StatusCode > 299 {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "gh: HTTP %d\n", resp.StatusCode)
|
||||
if msg := api.ScopesSuggestion(resp); msg != "" {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "gh: %s\n", msg)
|
||||
}
|
||||
err = cmdutil.SilentError
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -150,9 +151,6 @@ func createRun(opts *CreateOptions) error {
|
|||
if err != nil {
|
||||
var httpError api.HTTPError
|
||||
if errors.As(err, &httpError) {
|
||||
if httpError.OAuthScopes != "" && !strings.Contains(httpError.OAuthScopes, "gist") {
|
||||
return fmt.Errorf("This command requires the 'gist' OAuth scope.\nPlease re-authenticate with: gh auth refresh -h %s -s gist", host)
|
||||
}
|
||||
if httpError.StatusCode == http.StatusUnprocessableEntity {
|
||||
if detectEmptyFiles(files) {
|
||||
fmt.Fprintf(errOut, "%s Failed to create gist: %s\n", cs.FailureIcon(), "a gist file cannot be blank")
|
||||
|
|
@ -248,29 +246,42 @@ func guessGistName(files map[string]*shared.GistFile) string {
|
|||
}
|
||||
|
||||
func createGist(client *http.Client, hostname, description string, public bool, files map[string]*shared.GistFile) (*shared.Gist, error) {
|
||||
path := "gists"
|
||||
|
||||
body := &shared.Gist{
|
||||
Description: description,
|
||||
Public: public,
|
||||
Files: files,
|
||||
}
|
||||
|
||||
result := shared.Gist{}
|
||||
|
||||
requestByte, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestBody := bytes.NewReader(requestByte)
|
||||
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
err = apiClient.REST(hostname, "POST", path, requestBody, &result)
|
||||
if err != nil {
|
||||
requestBody := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(requestBody)
|
||||
if err := enc.Encode(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
u := ghinstance.RESTPrefix(hostname) + "gists"
|
||||
req, err := http.NewRequest(http.MethodPost, u, requestBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
return nil, api.HandleHTTPError(api.EndpointNeedsScopes(resp, "gist"))
|
||||
}
|
||||
|
||||
result := &shared.Gist{}
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
if err := dec.Decode(result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func detectEmptyFiles(files map[string]*shared.GistFile) bool {
|
||||
|
|
|
|||
|
|
@ -388,32 +388,3 @@ func Test_detectEmptyFiles(t *testing.T) {
|
|||
assert.Equal(t, tt.isEmptyFile, isEmptyFile)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CreateRun_reauth(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(httpmock.REST("POST", "gists"), func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 404,
|
||||
Request: req,
|
||||
Header: map[string][]string{
|
||||
"X-Oauth-Scopes": {"repo, read:org"},
|
||||
},
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString("oh no")),
|
||||
}, nil
|
||||
})
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
opts := &CreateOptions{
|
||||
IO: io,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
}
|
||||
|
||||
err := createRun(opts)
|
||||
assert.EqualError(t, err, "This command requires the 'gist' OAuth scope.\nPlease re-authenticate with: gh auth refresh -h github.com -s gist")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,5 +162,6 @@ func httpResponse(status int, req *http.Request, body io.Reader) *http.Response
|
|||
StatusCode: status,
|
||||
Request: req,
|
||||
Body: ioutil.NopCloser(body),
|
||||
Header: http.Header{},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue