@@ -142,11 +142,12 @@ func (gr GraphQLErrorResponse) Error() string {
142142
143143// HTTPError is an error returned by a failed API call
144144type HTTPError struct {
145- StatusCode int
146- RequestURL * url.URL
147- Message string
148- OAuthScopes string
149- Errors []HTTPErrorItem
145+ StatusCode int
146+ RequestURL * url.URL
147+ Message string
148+ Errors []HTTPErrorItem
149+
150+ scopesSuggestion string
150151}
151152
152153type HTTPErrorItem struct {
@@ -165,6 +166,61 @@ func (err HTTPError) Error() string {
165166 return fmt .Sprintf ("HTTP %d (%s)" , err .StatusCode , err .RequestURL )
166167}
167168
169+ func (err HTTPError ) ScopesSuggestion () string {
170+ return err .scopesSuggestion
171+ }
172+
173+ // ScopesSuggestion is an error messaging utility that prints the suggestion to request additional OAuth
174+ // scopes in case a server response indicates that there are missing scopes.
175+ func ScopesSuggestion (resp * http.Response ) string {
176+ if resp .StatusCode < 400 || resp .StatusCode > 499 {
177+ return ""
178+ }
179+
180+ endpointNeedsScopes := resp .Header .Get ("X-Accepted-Oauth-Scopes" )
181+ tokenHasScopes := resp .Header .Get ("X-Oauth-Scopes" )
182+ if tokenHasScopes == "" {
183+ return ""
184+ }
185+
186+ gotScopes := map [string ]struct {}{}
187+ for _ , s := range strings .Split (tokenHasScopes , "," ) {
188+ s = strings .TrimSpace (s )
189+ gotScopes [s ] = struct {}{}
190+ if strings .HasPrefix (s , "admin:" ) {
191+ gotScopes ["read:" + strings .TrimPrefix (s , "admin:" )] = struct {}{}
192+ gotScopes ["write:" + strings .TrimPrefix (s , "admin:" )] = struct {}{}
193+ } else if strings .HasPrefix (s , "write:" ) {
194+ gotScopes ["read:" + strings .TrimPrefix (s , "write:" )] = struct {}{}
195+ }
196+ }
197+
198+ for _ , s := range strings .Split (endpointNeedsScopes , "," ) {
199+ s = strings .TrimSpace (s )
200+ if _ , gotScope := gotScopes [s ]; s == "" || gotScope {
201+ continue
202+ }
203+ return fmt .Sprintf (
204+ "This API operation needs the %[1]q scope. To request it, run: gh auth refresh -h %[2]s -s %[1]s" ,
205+ s ,
206+ ghinstance .NormalizeHostname (resp .Request .URL .Hostname ()),
207+ )
208+ }
209+
210+ return ""
211+ }
212+
213+ // EndpointNeedsScopes adds additional OAuth scopes to an HTTP response as if they were returned from the
214+ // server endpoint. This improves HTTP 4xx error messaging for endpoints that don't explicitly list the
215+ // OAuth scopes they need.
216+ func EndpointNeedsScopes (resp * http.Response , s string ) * http.Response {
217+ if resp .StatusCode >= 400 && resp .StatusCode < 500 {
218+ oldScopes := resp .Header .Get ("X-Accepted-Oauth-Scopes" )
219+ resp .Header .Set ("X-Accepted-Oauth-Scopes" , fmt .Sprintf ("%s, %s" , oldScopes , s ))
220+ }
221+ return resp
222+ }
223+
168224// GraphQL performs a GraphQL request and parses the response
169225func (c Client ) GraphQL (hostname string , query string , variables map [string ]interface {}, data interface {}) error {
170226 reqBody , err := json .Marshal (map [string ]interface {}{"query" : query , "variables" : variables })
@@ -261,9 +317,9 @@ func handleResponse(resp *http.Response, data interface{}) error {
261317
262318func HandleHTTPError (resp * http.Response ) error {
263319 httpError := HTTPError {
264- StatusCode : resp .StatusCode ,
265- RequestURL : resp .Request .URL ,
266- OAuthScopes : resp . Header . Get ( "X-Oauth-Scopes" ),
320+ StatusCode : resp .StatusCode ,
321+ RequestURL : resp .Request .URL ,
322+ scopesSuggestion : ScopesSuggestion ( resp ),
267323 }
268324
269325 if ! jsonTypeRE .MatchString (resp .Header .Get ("Content-Type" )) {
0 commit comments