github.com/google/go-github/v66@v66.0.0/github/github.go (about) 1 // Copyright 2013 The go-github AUTHORS. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 //go:generate go run gen-accessors.go 7 //go:generate go run gen-stringify-test.go 8 //go:generate ../script/metadata.sh update-go 9 10 package github 11 12 import ( 13 "bytes" 14 "context" 15 "encoding/json" 16 "errors" 17 "fmt" 18 "io" 19 "net/http" 20 "net/url" 21 "reflect" 22 "strconv" 23 "strings" 24 "sync" 25 "time" 26 27 "github.com/google/go-querystring/query" 28 ) 29 30 const ( 31 Version = "v66.0.0" 32 33 defaultAPIVersion = "2022-11-28" 34 defaultBaseURL = "https://api.github.com/" 35 defaultUserAgent = "go-github" + "/" + Version 36 uploadBaseURL = "https://uploads.github.com/" 37 38 headerAPIVersion = "X-GitHub-Api-Version" 39 headerRateLimit = "X-RateLimit-Limit" 40 headerRateRemaining = "X-RateLimit-Remaining" 41 headerRateReset = "X-RateLimit-Reset" 42 headerOTP = "X-GitHub-OTP" 43 headerRetryAfter = "Retry-After" 44 45 headerTokenExpiration = "GitHub-Authentication-Token-Expiration" 46 47 mediaTypeV3 = "application/vnd.github.v3+json" 48 defaultMediaType = "application/octet-stream" 49 mediaTypeV3SHA = "application/vnd.github.v3.sha" 50 mediaTypeV3Diff = "application/vnd.github.v3.diff" 51 mediaTypeV3Patch = "application/vnd.github.v3.patch" 52 mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json" 53 mediaTypeIssueImportAPI = "application/vnd.github.golden-comet-preview+json" 54 55 // Media Type values to access preview APIs 56 // These media types will be added to the API request as headers 57 // and used to enable particular features on GitHub API that are still in preview. 58 // After some time, specific media types will be promoted (to a "stable" state). 59 // From then on, the preview headers are not required anymore to activate the additional 60 // feature on GitHub.com's API. However, this API header might still be needed for users 61 // to run a GitHub Enterprise Server on-premise. 62 // It's not uncommon for GitHub Enterprise Server customers to run older versions which 63 // would probably rely on the preview headers for some time. 64 // While the header promotion is going out for GitHub.com, it may be some time before it 65 // even arrives in GitHub Enterprise Server. 66 // We keep those preview headers around to avoid breaking older GitHub Enterprise Server 67 // versions. Additionally, non-functional (preview) headers don't create any side effects 68 // on GitHub Cloud version. 69 // 70 // See https://github.com/google/go-github/pull/2125 for full context. 71 72 // https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/ 73 mediaTypeStarringPreview = "application/vnd.github.v3.star+json" 74 75 // https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/ 76 mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json" 77 78 // https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/ 79 mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json" 80 81 // https://developer.github.com/changes/2018-10-16-deployments-environments-states-and-auto-inactive-updates/ 82 mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json" 83 84 // https://developer.github.com/changes/2016-05-12-reactions-api-preview/ 85 mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview" 86 87 // https://developer.github.com/changes/2016-05-23-timeline-preview-api/ 88 mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json" 89 90 // https://developer.github.com/changes/2016-09-14-projects-api/ 91 mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json" 92 93 // https://developer.github.com/changes/2017-01-05-commit-search-api/ 94 mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json" 95 96 // https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/ 97 mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json" 98 99 // https://developer.github.com/changes/2017-05-23-coc-api/ 100 mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json" 101 102 // https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/ 103 mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json" 104 105 // https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/ 106 mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json" 107 108 // https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/ 109 mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json" 110 111 // https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/ 112 mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview" 113 114 // https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/ 115 mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json" 116 117 // https://developer.github.com/changes/2018-09-05-project-card-events/ 118 mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json" 119 120 // https://developer.github.com/changes/2018-12-18-interactions-preview/ 121 mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json" 122 123 // https://developer.github.com/changes/2019-03-14-enabling-disabling-pages/ 124 mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json" 125 126 // https://developer.github.com/changes/2019-04-24-vulnerability-alerts/ 127 mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json" 128 129 // https://developer.github.com/changes/2019-05-29-update-branch-api/ 130 mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json" 131 132 // https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/ 133 mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json" 134 135 // https://docs.github.com/rest/previews/#repository-creation-permissions 136 mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json" 137 138 // https://docs.github.com/rest/previews/#create-and-use-repository-templates 139 mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json" 140 141 // https://developer.github.com/changes/2019-10-03-multi-line-comments/ 142 mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json" 143 144 // https://developer.github.com/changes/2019-11-05-deprecated-passwords-and-authorizations-api/ 145 mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json" 146 147 // https://developer.github.com/changes/2019-12-03-internal-visibility-changes/ 148 mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-preview+json" 149 150 // https://developer.github.com/changes/2018-12-10-content-attachments-api/ 151 mediaTypeContentAttachmentsPreview = "application/vnd.github.corsair-preview+json" 152 ) 153 154 var errNonNilContext = errors.New("context must be non-nil") 155 156 // A Client manages communication with the GitHub API. 157 type Client struct { 158 clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func. 159 client *http.Client // HTTP client used to communicate with the API. 160 161 // Base URL for API requests. Defaults to the public GitHub API, but can be 162 // set to a domain endpoint to use with GitHub Enterprise. BaseURL should 163 // always be specified with a trailing slash. 164 BaseURL *url.URL 165 166 // Base URL for uploading files. 167 UploadURL *url.URL 168 169 // User agent used when communicating with the GitHub API. 170 UserAgent string 171 172 rateMu sync.Mutex 173 rateLimits [Categories]Rate // Rate limits for the client as determined by the most recent API calls. 174 secondaryRateLimitReset time.Time // Secondary rate limit reset for the client as determined by the most recent API calls. 175 176 common service // Reuse a single struct instead of allocating one for each service on the heap. 177 178 // Services used for talking to different parts of the GitHub API. 179 Actions *ActionsService 180 Activity *ActivityService 181 Admin *AdminService 182 Apps *AppsService 183 Authorizations *AuthorizationsService 184 Billing *BillingService 185 Checks *ChecksService 186 CodeScanning *CodeScanningService 187 CodesOfConduct *CodesOfConductService 188 Codespaces *CodespacesService 189 Copilot *CopilotService 190 Dependabot *DependabotService 191 DependencyGraph *DependencyGraphService 192 Emojis *EmojisService 193 Enterprise *EnterpriseService 194 Gists *GistsService 195 Git *GitService 196 Gitignores *GitignoresService 197 Interactions *InteractionsService 198 IssueImport *IssueImportService 199 Issues *IssuesService 200 Licenses *LicensesService 201 Markdown *MarkdownService 202 Marketplace *MarketplaceService 203 Meta *MetaService 204 Migrations *MigrationService 205 Organizations *OrganizationsService 206 Projects *ProjectsService 207 PullRequests *PullRequestsService 208 RateLimit *RateLimitService 209 Reactions *ReactionsService 210 Repositories *RepositoriesService 211 SCIM *SCIMService 212 Search *SearchService 213 SecretScanning *SecretScanningService 214 SecurityAdvisories *SecurityAdvisoriesService 215 Teams *TeamsService 216 Users *UsersService 217 } 218 219 type service struct { 220 client *Client 221 } 222 223 // Client returns the http.Client used by this GitHub client. 224 // This should only be used for requests to the GitHub API because 225 // request headers will contain an authorization token. 226 func (c *Client) Client() *http.Client { 227 c.clientMu.Lock() 228 defer c.clientMu.Unlock() 229 clientCopy := *c.client 230 return &clientCopy 231 } 232 233 // ListOptions specifies the optional parameters to various List methods that 234 // support offset pagination. 235 type ListOptions struct { 236 // For paginated result sets, page of results to retrieve. 237 Page int `url:"page,omitempty"` 238 239 // For paginated result sets, the number of results to include per page. 240 PerPage int `url:"per_page,omitempty"` 241 } 242 243 // ListCursorOptions specifies the optional parameters to various List methods that 244 // support cursor pagination. 245 type ListCursorOptions struct { 246 // For paginated result sets, page of results to retrieve. 247 Page string `url:"page,omitempty"` 248 249 // For paginated result sets, the number of results to include per page. 250 PerPage int `url:"per_page,omitempty"` 251 252 // For paginated result sets, the number of results per page (max 100), starting from the first matching result. 253 // This parameter must not be used in combination with last. 254 First int `url:"first,omitempty"` 255 256 // For paginated result sets, the number of results per page (max 100), starting from the last matching result. 257 // This parameter must not be used in combination with first. 258 Last int `url:"last,omitempty"` 259 260 // A cursor, as given in the Link header. If specified, the query only searches for events after this cursor. 261 After string `url:"after,omitempty"` 262 263 // A cursor, as given in the Link header. If specified, the query only searches for events before this cursor. 264 Before string `url:"before,omitempty"` 265 266 // A cursor, as given in the Link header. If specified, the query continues the search using this cursor. 267 Cursor string `url:"cursor,omitempty"` 268 } 269 270 // UploadOptions specifies the parameters to methods that support uploads. 271 type UploadOptions struct { 272 Name string `url:"name,omitempty"` 273 Label string `url:"label,omitempty"` 274 MediaType string `url:"-"` 275 } 276 277 // RawType represents type of raw format of a request instead of JSON. 278 type RawType uint8 279 280 const ( 281 // Diff format. 282 Diff RawType = 1 + iota 283 // Patch format. 284 Patch 285 ) 286 287 // RawOptions specifies parameters when user wants to get raw format of 288 // a response instead of JSON. 289 type RawOptions struct { 290 Type RawType 291 } 292 293 // addOptions adds the parameters in opts as URL query parameters to s. opts 294 // must be a struct whose fields may contain "url" tags. 295 func addOptions(s string, opts interface{}) (string, error) { 296 v := reflect.ValueOf(opts) 297 if v.Kind() == reflect.Ptr && v.IsNil() { 298 return s, nil 299 } 300 301 u, err := url.Parse(s) 302 if err != nil { 303 return s, err 304 } 305 306 qs, err := query.Values(opts) 307 if err != nil { 308 return s, err 309 } 310 311 u.RawQuery = qs.Encode() 312 return u.String(), nil 313 } 314 315 // NewClient returns a new GitHub API client. If a nil httpClient is 316 // provided, a new http.Client will be used. To use API methods which require 317 // authentication, either use Client.WithAuthToken or provide NewClient with 318 // an http.Client that will perform the authentication for you (such as that 319 // provided by the golang.org/x/oauth2 library). 320 func NewClient(httpClient *http.Client) *Client { 321 if httpClient == nil { 322 httpClient = &http.Client{} 323 } 324 httpClient2 := *httpClient 325 c := &Client{client: &httpClient2} 326 c.initialize() 327 return c 328 } 329 330 // WithAuthToken returns a copy of the client configured to use the provided token for the Authorization header. 331 func (c *Client) WithAuthToken(token string) *Client { 332 c2 := c.copy() 333 defer c2.initialize() 334 transport := c2.client.Transport 335 if transport == nil { 336 transport = http.DefaultTransport 337 } 338 c2.client.Transport = roundTripperFunc( 339 func(req *http.Request) (*http.Response, error) { 340 req = req.Clone(req.Context()) 341 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 342 return transport.RoundTrip(req) 343 }, 344 ) 345 return c2 346 } 347 348 // WithEnterpriseURLs returns a copy of the client configured to use the provided base and 349 // upload URLs. If the base URL does not have the suffix "/api/v3/", it will be added 350 // automatically. If the upload URL does not have the suffix "/api/uploads", it will be 351 // added automatically. 352 // 353 // Note that WithEnterpriseURLs is a convenience helper only; 354 // its behavior is equivalent to setting the BaseURL and UploadURL fields. 355 // 356 // Another important thing is that by default, the GitHub Enterprise URL format 357 // should be http(s)://[hostname]/api/v3/ or you will always receive the 406 status code. 358 // The upload URL format should be http(s)://[hostname]/api/uploads/. 359 func (c *Client) WithEnterpriseURLs(baseURL, uploadURL string) (*Client, error) { 360 c2 := c.copy() 361 defer c2.initialize() 362 var err error 363 c2.BaseURL, err = url.Parse(baseURL) 364 if err != nil { 365 return nil, err 366 } 367 368 if !strings.HasSuffix(c2.BaseURL.Path, "/") { 369 c2.BaseURL.Path += "/" 370 } 371 if !strings.HasSuffix(c2.BaseURL.Path, "/api/v3/") && 372 !strings.HasPrefix(c2.BaseURL.Host, "api.") && 373 !strings.Contains(c2.BaseURL.Host, ".api.") { 374 c2.BaseURL.Path += "api/v3/" 375 } 376 377 c2.UploadURL, err = url.Parse(uploadURL) 378 if err != nil { 379 return nil, err 380 } 381 382 if !strings.HasSuffix(c2.UploadURL.Path, "/") { 383 c2.UploadURL.Path += "/" 384 } 385 if !strings.HasSuffix(c2.UploadURL.Path, "/api/uploads/") && 386 !strings.HasPrefix(c2.UploadURL.Host, "api.") && 387 !strings.Contains(c2.UploadURL.Host, ".api.") { 388 c2.UploadURL.Path += "api/uploads/" 389 } 390 return c2, nil 391 } 392 393 // initialize sets default values and initializes services. 394 func (c *Client) initialize() { 395 if c.client == nil { 396 c.client = &http.Client{} 397 } 398 if c.BaseURL == nil { 399 c.BaseURL, _ = url.Parse(defaultBaseURL) 400 } 401 if c.UploadURL == nil { 402 c.UploadURL, _ = url.Parse(uploadBaseURL) 403 } 404 if c.UserAgent == "" { 405 c.UserAgent = defaultUserAgent 406 } 407 c.common.client = c 408 c.Actions = (*ActionsService)(&c.common) 409 c.Activity = (*ActivityService)(&c.common) 410 c.Admin = (*AdminService)(&c.common) 411 c.Apps = (*AppsService)(&c.common) 412 c.Authorizations = (*AuthorizationsService)(&c.common) 413 c.Billing = (*BillingService)(&c.common) 414 c.Checks = (*ChecksService)(&c.common) 415 c.CodeScanning = (*CodeScanningService)(&c.common) 416 c.Codespaces = (*CodespacesService)(&c.common) 417 c.CodesOfConduct = (*CodesOfConductService)(&c.common) 418 c.Copilot = (*CopilotService)(&c.common) 419 c.Dependabot = (*DependabotService)(&c.common) 420 c.DependencyGraph = (*DependencyGraphService)(&c.common) 421 c.Emojis = (*EmojisService)(&c.common) 422 c.Enterprise = (*EnterpriseService)(&c.common) 423 c.Gists = (*GistsService)(&c.common) 424 c.Git = (*GitService)(&c.common) 425 c.Gitignores = (*GitignoresService)(&c.common) 426 c.Interactions = (*InteractionsService)(&c.common) 427 c.IssueImport = (*IssueImportService)(&c.common) 428 c.Issues = (*IssuesService)(&c.common) 429 c.Licenses = (*LicensesService)(&c.common) 430 c.Markdown = (*MarkdownService)(&c.common) 431 c.Marketplace = &MarketplaceService{client: c} 432 c.Meta = (*MetaService)(&c.common) 433 c.Migrations = (*MigrationService)(&c.common) 434 c.Organizations = (*OrganizationsService)(&c.common) 435 c.Projects = (*ProjectsService)(&c.common) 436 c.PullRequests = (*PullRequestsService)(&c.common) 437 c.RateLimit = (*RateLimitService)(&c.common) 438 c.Reactions = (*ReactionsService)(&c.common) 439 c.Repositories = (*RepositoriesService)(&c.common) 440 c.SCIM = (*SCIMService)(&c.common) 441 c.Search = (*SearchService)(&c.common) 442 c.SecretScanning = (*SecretScanningService)(&c.common) 443 c.SecurityAdvisories = (*SecurityAdvisoriesService)(&c.common) 444 c.Teams = (*TeamsService)(&c.common) 445 c.Users = (*UsersService)(&c.common) 446 } 447 448 // copy returns a copy of the current client. It must be initialized before use. 449 func (c *Client) copy() *Client { 450 c.clientMu.Lock() 451 // can't use *c here because that would copy mutexes by value. 452 clone := Client{ 453 client: &http.Client{}, 454 UserAgent: c.UserAgent, 455 BaseURL: c.BaseURL, 456 UploadURL: c.UploadURL, 457 secondaryRateLimitReset: c.secondaryRateLimitReset, 458 } 459 c.clientMu.Unlock() 460 if c.client != nil { 461 clone.client.Transport = c.client.Transport 462 clone.client.CheckRedirect = c.client.CheckRedirect 463 clone.client.Jar = c.client.Jar 464 clone.client.Timeout = c.client.Timeout 465 } 466 c.rateMu.Lock() 467 copy(clone.rateLimits[:], c.rateLimits[:]) 468 c.rateMu.Unlock() 469 return &clone 470 } 471 472 // NewClientWithEnvProxy enhances NewClient with the HttpProxy env. 473 func NewClientWithEnvProxy() *Client { 474 return NewClient(&http.Client{Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}}) 475 } 476 477 // NewTokenClient returns a new GitHub API client authenticated with the provided token. 478 // Deprecated: Use NewClient(nil).WithAuthToken(token) instead. 479 func NewTokenClient(_ context.Context, token string) *Client { 480 // This always returns a nil error. 481 return NewClient(nil).WithAuthToken(token) 482 } 483 484 // NewEnterpriseClient returns a new GitHub API client with provided 485 // base URL and upload URL (often is your GitHub Enterprise hostname). 486 // 487 // Deprecated: Use NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) instead. 488 func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) { 489 return NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) 490 } 491 492 // RequestOption represents an option that can modify an http.Request. 493 type RequestOption func(req *http.Request) 494 495 // WithVersion overrides the GitHub v3 API version for this individual request. 496 // For more information, see: 497 // https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/ 498 func WithVersion(version string) RequestOption { 499 return func(req *http.Request) { 500 req.Header.Set(headerAPIVersion, version) 501 } 502 } 503 504 // NewRequest creates an API request. A relative URL can be provided in urlStr, 505 // in which case it is resolved relative to the BaseURL of the Client. 506 // Relative URLs should always be specified without a preceding slash. If 507 // specified, the value pointed to by body is JSON encoded and included as the 508 // request body. 509 func (c *Client) NewRequest(method, urlStr string, body interface{}, opts ...RequestOption) (*http.Request, error) { 510 if !strings.HasSuffix(c.BaseURL.Path, "/") { 511 return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL) 512 } 513 514 u, err := c.BaseURL.Parse(urlStr) 515 if err != nil { 516 return nil, err 517 } 518 519 var buf io.ReadWriter 520 if body != nil { 521 buf = &bytes.Buffer{} 522 enc := json.NewEncoder(buf) 523 enc.SetEscapeHTML(false) 524 err := enc.Encode(body) 525 if err != nil { 526 return nil, err 527 } 528 } 529 530 req, err := http.NewRequest(method, u.String(), buf) 531 if err != nil { 532 return nil, err 533 } 534 535 if body != nil { 536 req.Header.Set("Content-Type", "application/json") 537 } 538 req.Header.Set("Accept", mediaTypeV3) 539 if c.UserAgent != "" { 540 req.Header.Set("User-Agent", c.UserAgent) 541 } 542 req.Header.Set(headerAPIVersion, defaultAPIVersion) 543 544 for _, opt := range opts { 545 opt(req) 546 } 547 548 return req, nil 549 } 550 551 // NewFormRequest creates an API request. A relative URL can be provided in urlStr, 552 // in which case it is resolved relative to the BaseURL of the Client. 553 // Relative URLs should always be specified without a preceding slash. 554 // Body is sent with Content-Type: application/x-www-form-urlencoded. 555 func (c *Client) NewFormRequest(urlStr string, body io.Reader, opts ...RequestOption) (*http.Request, error) { 556 if !strings.HasSuffix(c.BaseURL.Path, "/") { 557 return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL) 558 } 559 560 u, err := c.BaseURL.Parse(urlStr) 561 if err != nil { 562 return nil, err 563 } 564 565 req, err := http.NewRequest(http.MethodPost, u.String(), body) 566 if err != nil { 567 return nil, err 568 } 569 570 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 571 req.Header.Set("Accept", mediaTypeV3) 572 if c.UserAgent != "" { 573 req.Header.Set("User-Agent", c.UserAgent) 574 } 575 req.Header.Set(headerAPIVersion, defaultAPIVersion) 576 577 for _, opt := range opts { 578 opt(req) 579 } 580 581 return req, nil 582 } 583 584 // NewUploadRequest creates an upload request. A relative URL can be provided in 585 // urlStr, in which case it is resolved relative to the UploadURL of the Client. 586 // Relative URLs should always be specified without a preceding slash. 587 func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string, opts ...RequestOption) (*http.Request, error) { 588 if !strings.HasSuffix(c.UploadURL.Path, "/") { 589 return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL) 590 } 591 u, err := c.UploadURL.Parse(urlStr) 592 if err != nil { 593 return nil, err 594 } 595 596 req, err := http.NewRequest("POST", u.String(), reader) 597 if err != nil { 598 return nil, err 599 } 600 601 req.ContentLength = size 602 603 if mediaType == "" { 604 mediaType = defaultMediaType 605 } 606 req.Header.Set("Content-Type", mediaType) 607 req.Header.Set("Accept", mediaTypeV3) 608 req.Header.Set("User-Agent", c.UserAgent) 609 req.Header.Set(headerAPIVersion, defaultAPIVersion) 610 611 for _, opt := range opts { 612 opt(req) 613 } 614 615 return req, nil 616 } 617 618 // Response is a GitHub API response. This wraps the standard http.Response 619 // returned from GitHub and provides convenient access to things like 620 // pagination links. 621 type Response struct { 622 *http.Response 623 624 // These fields provide the page values for paginating through a set of 625 // results. Any or all of these may be set to the zero value for 626 // responses that are not part of a paginated set, or for which there 627 // are no additional pages. 628 // 629 // These fields support what is called "offset pagination" and should 630 // be used with the ListOptions struct. 631 NextPage int 632 PrevPage int 633 FirstPage int 634 LastPage int 635 636 // Additionally, some APIs support "cursor pagination" instead of offset. 637 // This means that a token points directly to the next record which 638 // can lead to O(1) performance compared to O(n) performance provided 639 // by offset pagination. 640 // 641 // For APIs that support cursor pagination (such as 642 // TeamsService.ListIDPGroupsInOrganization), the following field 643 // will be populated to point to the next page. 644 // 645 // To use this token, set ListCursorOptions.Page to this value before 646 // calling the endpoint again. 647 NextPageToken string 648 649 // For APIs that support cursor pagination, such as RepositoriesService.ListHookDeliveries, 650 // the following field will be populated to point to the next page. 651 // Set ListCursorOptions.Cursor to this value when calling the endpoint again. 652 Cursor string 653 654 // For APIs that support before/after pagination, such as OrganizationsService.AuditLog. 655 Before string 656 After string 657 658 // Explicitly specify the Rate type so Rate's String() receiver doesn't 659 // propagate to Response. 660 Rate Rate 661 662 // token's expiration date. Timestamp is 0001-01-01 when token doesn't expire. 663 // So it is valid for TokenExpiration.Equal(Timestamp{}) or TokenExpiration.Time.After(time.Now()) 664 TokenExpiration Timestamp 665 } 666 667 // newResponse creates a new Response for the provided http.Response. 668 // r must not be nil. 669 func newResponse(r *http.Response) *Response { 670 response := &Response{Response: r} 671 response.populatePageValues() 672 response.Rate = parseRate(r) 673 response.TokenExpiration = parseTokenExpiration(r) 674 return response 675 } 676 677 // populatePageValues parses the HTTP Link response headers and populates the 678 // various pagination link values in the Response. 679 func (r *Response) populatePageValues() { 680 if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 { 681 for _, link := range strings.Split(links[0], ",") { 682 segments := strings.Split(strings.TrimSpace(link), ";") 683 684 // link must at least have href and rel 685 if len(segments) < 2 { 686 continue 687 } 688 689 // ensure href is properly formatted 690 if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") { 691 continue 692 } 693 694 // try to pull out page parameter 695 url, err := url.Parse(segments[0][1 : len(segments[0])-1]) 696 if err != nil { 697 continue 698 } 699 700 q := url.Query() 701 702 if cursor := q.Get("cursor"); cursor != "" { 703 for _, segment := range segments[1:] { 704 switch strings.TrimSpace(segment) { 705 case `rel="next"`: 706 r.Cursor = cursor 707 } 708 } 709 710 continue 711 } 712 713 page := q.Get("page") 714 since := q.Get("since") 715 before := q.Get("before") 716 after := q.Get("after") 717 718 if page == "" && before == "" && after == "" && since == "" { 719 continue 720 } 721 722 if since != "" && page == "" { 723 page = since 724 } 725 726 for _, segment := range segments[1:] { 727 switch strings.TrimSpace(segment) { 728 case `rel="next"`: 729 if r.NextPage, err = strconv.Atoi(page); err != nil { 730 r.NextPageToken = page 731 } 732 r.After = after 733 case `rel="prev"`: 734 r.PrevPage, _ = strconv.Atoi(page) 735 r.Before = before 736 case `rel="first"`: 737 r.FirstPage, _ = strconv.Atoi(page) 738 case `rel="last"`: 739 r.LastPage, _ = strconv.Atoi(page) 740 } 741 } 742 } 743 } 744 } 745 746 // parseRate parses the rate related headers. 747 func parseRate(r *http.Response) Rate { 748 var rate Rate 749 if limit := r.Header.Get(headerRateLimit); limit != "" { 750 rate.Limit, _ = strconv.Atoi(limit) 751 } 752 if remaining := r.Header.Get(headerRateRemaining); remaining != "" { 753 rate.Remaining, _ = strconv.Atoi(remaining) 754 } 755 if reset := r.Header.Get(headerRateReset); reset != "" { 756 if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { 757 rate.Reset = Timestamp{time.Unix(v, 0)} 758 } 759 } 760 return rate 761 } 762 763 // parseSecondaryRate parses the secondary rate related headers, 764 // and returns the time to retry after. 765 func parseSecondaryRate(r *http.Response) *time.Duration { 766 // According to GitHub support, the "Retry-After" header value will be 767 // an integer which represents the number of seconds that one should 768 // wait before resuming making requests. 769 if v := r.Header.Get(headerRetryAfter); v != "" { 770 retryAfterSeconds, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop. 771 retryAfter := time.Duration(retryAfterSeconds) * time.Second 772 return &retryAfter 773 } 774 775 // According to GitHub support, endpoints might return x-ratelimit-reset instead, 776 // as an integer which represents the number of seconds since epoch UTC, 777 // representing the time to resume making requests. 778 if v := r.Header.Get(headerRateReset); v != "" { 779 secondsSinceEpoch, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop. 780 retryAfter := time.Until(time.Unix(secondsSinceEpoch, 0)) 781 return &retryAfter 782 } 783 784 return nil 785 } 786 787 // parseTokenExpiration parses the TokenExpiration related headers. 788 // Returns 0001-01-01 if the header is not defined or could not be parsed. 789 func parseTokenExpiration(r *http.Response) Timestamp { 790 if v := r.Header.Get(headerTokenExpiration); v != "" { 791 if t, err := time.Parse("2006-01-02 15:04:05 MST", v); err == nil { 792 return Timestamp{t.Local()} 793 } 794 // Some tokens include the timezone offset instead of the timezone. 795 // https://github.com/google/go-github/issues/2649 796 if t, err := time.Parse("2006-01-02 15:04:05 -0700", v); err == nil { 797 return Timestamp{t.Local()} 798 } 799 } 800 return Timestamp{} // 0001-01-01 00:00:00 801 } 802 803 type requestContext uint8 804 805 const ( 806 bypassRateLimitCheck requestContext = iota 807 SleepUntilPrimaryRateLimitResetWhenRateLimited 808 ) 809 810 // BareDo sends an API request and lets you handle the api response. If an error 811 // or API Error occurs, the error will contain more information. Otherwise you 812 // are supposed to read and close the response's Body. If rate limit is exceeded 813 // and reset time is in the future, BareDo returns *RateLimitError immediately 814 // without making a network API call. 815 // 816 // The provided ctx must be non-nil, if it is nil an error is returned. If it is 817 // canceled or times out, ctx.Err() will be returned. 818 func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, error) { 819 if ctx == nil { 820 return nil, errNonNilContext 821 } 822 823 req = withContext(ctx, req) 824 825 rateLimitCategory := GetRateLimitCategory(req.Method, req.URL.Path) 826 827 if bypass := ctx.Value(bypassRateLimitCheck); bypass == nil { 828 // If we've hit rate limit, don't make further requests before Reset time. 829 if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil { 830 return &Response{ 831 Response: err.Response, 832 Rate: err.Rate, 833 }, err 834 } 835 // If we've hit a secondary rate limit, don't make further requests before Retry After. 836 if err := c.checkSecondaryRateLimitBeforeDo(req); err != nil { 837 return &Response{ 838 Response: err.Response, 839 }, err 840 } 841 } 842 843 resp, err := c.client.Do(req) 844 if err != nil { 845 // If we got an error, and the context has been canceled, 846 // the context's error is probably more useful. 847 select { 848 case <-ctx.Done(): 849 return nil, ctx.Err() 850 default: 851 } 852 853 // If the error type is *url.Error, sanitize its URL before returning. 854 if e, ok := err.(*url.Error); ok { 855 if url, err := url.Parse(e.URL); err == nil { 856 e.URL = sanitizeURL(url).String() 857 return nil, e 858 } 859 } 860 861 return nil, err 862 } 863 864 response := newResponse(resp) 865 866 // Don't update the rate limits if this was a cached response. 867 // X-From-Cache is set by https://github.com/gregjones/httpcache 868 if response.Header.Get("X-From-Cache") == "" { 869 c.rateMu.Lock() 870 c.rateLimits[rateLimitCategory] = response.Rate 871 c.rateMu.Unlock() 872 } 873 874 err = CheckResponse(resp) 875 if err != nil { 876 defer resp.Body.Close() 877 // Special case for AcceptedErrors. If an AcceptedError 878 // has been encountered, the response's payload will be 879 // added to the AcceptedError and returned. 880 // 881 // Issue #1022 882 aerr, ok := err.(*AcceptedError) 883 if ok { 884 b, readErr := io.ReadAll(resp.Body) 885 if readErr != nil { 886 return response, readErr 887 } 888 889 aerr.Raw = b 890 err = aerr 891 } 892 893 rateLimitError, ok := err.(*RateLimitError) 894 if ok && req.Context().Value(SleepUntilPrimaryRateLimitResetWhenRateLimited) != nil { 895 if err := sleepUntilResetWithBuffer(req.Context(), rateLimitError.Rate.Reset.Time); err != nil { 896 return response, err 897 } 898 // retry the request once when the rate limit has reset 899 return c.BareDo(context.WithValue(req.Context(), SleepUntilPrimaryRateLimitResetWhenRateLimited, nil), req) 900 } 901 902 // Update the secondary rate limit if we hit it. 903 rerr, ok := err.(*AbuseRateLimitError) 904 if ok && rerr.RetryAfter != nil { 905 c.rateMu.Lock() 906 c.secondaryRateLimitReset = time.Now().Add(*rerr.RetryAfter) 907 c.rateMu.Unlock() 908 } 909 } 910 return response, err 911 } 912 913 // Do sends an API request and returns the API response. The API response is 914 // JSON decoded and stored in the value pointed to by v, or returned as an 915 // error if an API error has occurred. If v implements the io.Writer interface, 916 // the raw response body will be written to v, without attempting to first 917 // decode it. If v is nil, and no error happens, the response is returned as is. 918 // If rate limit is exceeded and reset time is in the future, Do returns 919 // *RateLimitError immediately without making a network API call. 920 // 921 // The provided ctx must be non-nil, if it is nil an error is returned. If it 922 // is canceled or times out, ctx.Err() will be returned. 923 func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { 924 resp, err := c.BareDo(ctx, req) 925 if err != nil { 926 return resp, err 927 } 928 defer resp.Body.Close() 929 930 switch v := v.(type) { 931 case nil: 932 case io.Writer: 933 _, err = io.Copy(v, resp.Body) 934 default: 935 decErr := json.NewDecoder(resp.Body).Decode(v) 936 if decErr == io.EOF { 937 decErr = nil // ignore EOF errors caused by empty response body 938 } 939 if decErr != nil { 940 err = decErr 941 } 942 } 943 return resp, err 944 } 945 946 // checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from 947 // current client state in order to quickly check if *RateLimitError can be immediately returned 948 // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. 949 // Otherwise it returns nil, and Client.Do should proceed normally. 950 func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory RateLimitCategory) *RateLimitError { 951 c.rateMu.Lock() 952 rate := c.rateLimits[rateLimitCategory] 953 c.rateMu.Unlock() 954 if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) { 955 // Create a fake response. 956 resp := &http.Response{ 957 Status: http.StatusText(http.StatusForbidden), 958 StatusCode: http.StatusForbidden, 959 Request: req, 960 Header: make(http.Header), 961 Body: io.NopCloser(strings.NewReader("")), 962 } 963 964 if req.Context().Value(SleepUntilPrimaryRateLimitResetWhenRateLimited) != nil { 965 if err := sleepUntilResetWithBuffer(req.Context(), rate.Reset.Time); err == nil { 966 return nil 967 } 968 return &RateLimitError{ 969 Rate: rate, 970 Response: resp, 971 Message: fmt.Sprintf("Context cancelled while waiting for rate limit to reset until %v, not making remote request.", rate.Reset.Time), 972 } 973 } 974 975 return &RateLimitError{ 976 Rate: rate, 977 Response: resp, 978 Message: fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time), 979 } 980 } 981 982 return nil 983 } 984 985 // checkSecondaryRateLimitBeforeDo does not make any network calls, but uses existing knowledge from 986 // current client state in order to quickly check if *AbuseRateLimitError can be immediately returned 987 // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. 988 // Otherwise it returns nil, and Client.Do should proceed normally. 989 func (c *Client) checkSecondaryRateLimitBeforeDo(req *http.Request) *AbuseRateLimitError { 990 c.rateMu.Lock() 991 secondary := c.secondaryRateLimitReset 992 c.rateMu.Unlock() 993 if !secondary.IsZero() && time.Now().Before(secondary) { 994 // Create a fake response. 995 resp := &http.Response{ 996 Status: http.StatusText(http.StatusForbidden), 997 StatusCode: http.StatusForbidden, 998 Request: req, 999 Header: make(http.Header), 1000 Body: io.NopCloser(strings.NewReader("")), 1001 } 1002 1003 retryAfter := time.Until(secondary) 1004 return &AbuseRateLimitError{ 1005 Response: resp, 1006 Message: fmt.Sprintf("API secondary rate limit exceeded until %v, not making remote request.", secondary), 1007 RetryAfter: &retryAfter, 1008 } 1009 } 1010 1011 return nil 1012 } 1013 1014 // compareHTTPResponse returns whether two http.Response objects are equal or not. 1015 // Currently, only StatusCode is checked. This function is used when implementing the 1016 // Is(error) bool interface for the custom error types in this package. 1017 func compareHTTPResponse(r1, r2 *http.Response) bool { 1018 if r1 == nil && r2 == nil { 1019 return true 1020 } 1021 1022 if r1 != nil && r2 != nil { 1023 return r1.StatusCode == r2.StatusCode 1024 } 1025 return false 1026 } 1027 1028 /* 1029 An ErrorResponse reports one or more errors caused by an API request. 1030 1031 GitHub API docs: https://docs.github.com/rest/#client-errors 1032 */ 1033 type ErrorResponse struct { 1034 Response *http.Response `json:"-"` // HTTP response that caused this error 1035 Message string `json:"message"` // error message 1036 Errors []Error `json:"errors"` // more detail on individual errors 1037 // Block is only populated on certain types of errors such as code 451. 1038 Block *ErrorBlock `json:"block,omitempty"` 1039 // Most errors will also include a documentation_url field pointing 1040 // to some content that might help you resolve the error, see 1041 // https://docs.github.com/rest/#client-errors 1042 DocumentationURL string `json:"documentation_url,omitempty"` 1043 } 1044 1045 // ErrorBlock contains a further explanation for the reason of an error. 1046 // See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/ 1047 // for more information. 1048 type ErrorBlock struct { 1049 Reason string `json:"reason,omitempty"` 1050 CreatedAt *Timestamp `json:"created_at,omitempty"` 1051 } 1052 1053 func (r *ErrorResponse) Error() string { 1054 if r.Response != nil && r.Response.Request != nil { 1055 return fmt.Sprintf("%v %v: %d %v %+v", 1056 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1057 r.Response.StatusCode, r.Message, r.Errors) 1058 } 1059 1060 if r.Response != nil { 1061 return fmt.Sprintf("%d %v %+v", r.Response.StatusCode, r.Message, r.Errors) 1062 } 1063 1064 return fmt.Sprintf("%v %+v", r.Message, r.Errors) 1065 } 1066 1067 // Is returns whether the provided error equals this error. 1068 func (r *ErrorResponse) Is(target error) bool { 1069 v, ok := target.(*ErrorResponse) 1070 if !ok { 1071 return false 1072 } 1073 1074 if r.Message != v.Message || (r.DocumentationURL != v.DocumentationURL) || 1075 !compareHTTPResponse(r.Response, v.Response) { 1076 return false 1077 } 1078 1079 // Compare Errors. 1080 if len(r.Errors) != len(v.Errors) { 1081 return false 1082 } 1083 for idx := range r.Errors { 1084 if r.Errors[idx] != v.Errors[idx] { 1085 return false 1086 } 1087 } 1088 1089 // Compare Block. 1090 if (r.Block != nil && v.Block == nil) || (r.Block == nil && v.Block != nil) { 1091 return false 1092 } 1093 if r.Block != nil && v.Block != nil { 1094 if r.Block.Reason != v.Block.Reason { 1095 return false 1096 } 1097 if (r.Block.CreatedAt != nil && v.Block.CreatedAt == nil) || (r.Block.CreatedAt == 1098 nil && v.Block.CreatedAt != nil) { 1099 return false 1100 } 1101 if r.Block.CreatedAt != nil && v.Block.CreatedAt != nil { 1102 if *(r.Block.CreatedAt) != *(v.Block.CreatedAt) { 1103 return false 1104 } 1105 } 1106 } 1107 1108 return true 1109 } 1110 1111 // TwoFactorAuthError occurs when using HTTP Basic Authentication for a user 1112 // that has two-factor authentication enabled. The request can be reattempted 1113 // by providing a one-time password in the request. 1114 type TwoFactorAuthError ErrorResponse 1115 1116 func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() } 1117 1118 // RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit 1119 // remaining value of 0. 1120 type RateLimitError struct { 1121 Rate Rate // Rate specifies last known rate limit for the client 1122 Response *http.Response // HTTP response that caused this error 1123 Message string `json:"message"` // error message 1124 } 1125 1126 func (r *RateLimitError) Error() string { 1127 return fmt.Sprintf("%v %v: %d %v %v", 1128 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1129 r.Response.StatusCode, r.Message, formatRateReset(time.Until(r.Rate.Reset.Time))) 1130 } 1131 1132 // Is returns whether the provided error equals this error. 1133 func (r *RateLimitError) Is(target error) bool { 1134 v, ok := target.(*RateLimitError) 1135 if !ok { 1136 return false 1137 } 1138 1139 return r.Rate == v.Rate && 1140 r.Message == v.Message && 1141 compareHTTPResponse(r.Response, v.Response) 1142 } 1143 1144 // AcceptedError occurs when GitHub returns 202 Accepted response with an 1145 // empty body, which means a job was scheduled on the GitHub side to process 1146 // the information needed and cache it. 1147 // Technically, 202 Accepted is not a real error, it's just used to 1148 // indicate that results are not ready yet, but should be available soon. 1149 // The request can be repeated after some time. 1150 type AcceptedError struct { 1151 // Raw contains the response body. 1152 Raw []byte 1153 } 1154 1155 func (*AcceptedError) Error() string { 1156 return "job scheduled on GitHub side; try again later" 1157 } 1158 1159 // Is returns whether the provided error equals this error. 1160 func (ae *AcceptedError) Is(target error) bool { 1161 v, ok := target.(*AcceptedError) 1162 if !ok { 1163 return false 1164 } 1165 return bytes.Equal(ae.Raw, v.Raw) 1166 } 1167 1168 // AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the 1169 // "documentation_url" field value equal to "https://docs.github.com/rest/overview/rate-limits-for-the-rest-api#about-secondary-rate-limits". 1170 type AbuseRateLimitError struct { 1171 Response *http.Response // HTTP response that caused this error 1172 Message string `json:"message"` // error message 1173 1174 // RetryAfter is provided with some abuse rate limit errors. If present, 1175 // it is the amount of time that the client should wait before retrying. 1176 // Otherwise, the client should try again later (after an unspecified amount of time). 1177 RetryAfter *time.Duration 1178 } 1179 1180 func (r *AbuseRateLimitError) Error() string { 1181 return fmt.Sprintf("%v %v: %d %v", 1182 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1183 r.Response.StatusCode, r.Message) 1184 } 1185 1186 // Is returns whether the provided error equals this error. 1187 func (r *AbuseRateLimitError) Is(target error) bool { 1188 v, ok := target.(*AbuseRateLimitError) 1189 if !ok { 1190 return false 1191 } 1192 1193 return r.Message == v.Message && 1194 r.RetryAfter == v.RetryAfter && 1195 compareHTTPResponse(r.Response, v.Response) 1196 } 1197 1198 // sanitizeURL redacts the client_secret parameter from the URL which may be 1199 // exposed to the user. 1200 func sanitizeURL(uri *url.URL) *url.URL { 1201 if uri == nil { 1202 return nil 1203 } 1204 params := uri.Query() 1205 if len(params.Get("client_secret")) > 0 { 1206 params.Set("client_secret", "REDACTED") 1207 uri.RawQuery = params.Encode() 1208 } 1209 return uri 1210 } 1211 1212 /* 1213 An Error reports more details on an individual error in an ErrorResponse. 1214 These are the possible validation error codes: 1215 1216 missing: 1217 resource does not exist 1218 missing_field: 1219 a required field on a resource has not been set 1220 invalid: 1221 the formatting of a field is invalid 1222 already_exists: 1223 another resource has the same valid as this field 1224 custom: 1225 some resources return this (e.g. github.User.CreateKey()), additional 1226 information is set in the Message field of the Error 1227 1228 GitHub error responses structure are often undocumented and inconsistent. 1229 Sometimes error is just a simple string (Issue #540). 1230 In such cases, Message represents an error message as a workaround. 1231 1232 GitHub API docs: https://docs.github.com/rest/#client-errors 1233 */ 1234 type Error struct { 1235 Resource string `json:"resource"` // resource on which the error occurred 1236 Field string `json:"field"` // field on which the error occurred 1237 Code string `json:"code"` // validation error code 1238 Message string `json:"message"` // Message describing the error. Errors with Code == "custom" will always have this set. 1239 } 1240 1241 func (e *Error) Error() string { 1242 return fmt.Sprintf("%v error caused by %v field on %v resource", 1243 e.Code, e.Field, e.Resource) 1244 } 1245 1246 func (e *Error) UnmarshalJSON(data []byte) error { 1247 type aliasError Error // avoid infinite recursion by using type alias. 1248 if err := json.Unmarshal(data, (*aliasError)(e)); err != nil { 1249 return json.Unmarshal(data, &e.Message) // data can be json string. 1250 } 1251 return nil 1252 } 1253 1254 // CheckResponse checks the API response for errors, and returns them if 1255 // present. A response is considered an error if it has a status code outside 1256 // the 200 range or equal to 202 Accepted. 1257 // API error responses are expected to have response 1258 // body, and a JSON response body that maps to ErrorResponse. 1259 // 1260 // The error type will be *RateLimitError for rate limit exceeded errors, 1261 // *AcceptedError for 202 Accepted status codes, 1262 // and *TwoFactorAuthError for two-factor authentication errors. 1263 func CheckResponse(r *http.Response) error { 1264 if r.StatusCode == http.StatusAccepted { 1265 return &AcceptedError{} 1266 } 1267 if c := r.StatusCode; 200 <= c && c <= 299 { 1268 return nil 1269 } 1270 1271 errorResponse := &ErrorResponse{Response: r} 1272 data, err := io.ReadAll(r.Body) 1273 if err == nil && data != nil { 1274 err = json.Unmarshal(data, errorResponse) 1275 if err != nil { 1276 // reset the response as if this never happened 1277 errorResponse = &ErrorResponse{Response: r} 1278 } 1279 } 1280 // Re-populate error response body because GitHub error responses are often 1281 // undocumented and inconsistent. 1282 // Issue #1136, #540. 1283 r.Body = io.NopCloser(bytes.NewBuffer(data)) 1284 switch { 1285 case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"): 1286 return (*TwoFactorAuthError)(errorResponse) 1287 case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0": 1288 return &RateLimitError{ 1289 Rate: parseRate(r), 1290 Response: errorResponse.Response, 1291 Message: errorResponse.Message, 1292 } 1293 case r.StatusCode == http.StatusForbidden && 1294 (strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits") || 1295 strings.HasSuffix(errorResponse.DocumentationURL, "secondary-rate-limits")): 1296 abuseRateLimitError := &AbuseRateLimitError{ 1297 Response: errorResponse.Response, 1298 Message: errorResponse.Message, 1299 } 1300 if retryAfter := parseSecondaryRate(r); retryAfter != nil { 1301 abuseRateLimitError.RetryAfter = retryAfter 1302 } 1303 return abuseRateLimitError 1304 default: 1305 return errorResponse 1306 } 1307 } 1308 1309 // parseBoolResponse determines the boolean result from a GitHub API response. 1310 // Several GitHub API methods return boolean responses indicated by the HTTP 1311 // status code in the response (true indicated by a 204, false indicated by a 1312 // 404). This helper function will determine that result and hide the 404 1313 // error if present. Any other error will be returned through as-is. 1314 func parseBoolResponse(err error) (bool, error) { 1315 if err == nil { 1316 return true, nil 1317 } 1318 1319 if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound { 1320 // Simply false. In this one case, we do not pass the error through. 1321 return false, nil 1322 } 1323 1324 // some other real error occurred 1325 return false, err 1326 } 1327 1328 type RateLimitCategory uint8 1329 1330 const ( 1331 CoreCategory RateLimitCategory = iota 1332 SearchCategory 1333 GraphqlCategory 1334 IntegrationManifestCategory 1335 SourceImportCategory 1336 CodeScanningUploadCategory 1337 ActionsRunnerRegistrationCategory 1338 ScimCategory 1339 DependencySnapshotsCategory 1340 CodeSearchCategory 1341 AuditLogCategory 1342 1343 Categories // An array of this length will be able to contain all rate limit categories. 1344 ) 1345 1346 // GetRateLimitCategory returns the rate limit RateLimitCategory of the endpoint, determined by HTTP method and Request.URL.Path. 1347 func GetRateLimitCategory(method, path string) RateLimitCategory { 1348 switch { 1349 // https://docs.github.com/rest/rate-limit#about-rate-limits 1350 default: 1351 // NOTE: coreCategory is returned for actionsRunnerRegistrationCategory too, 1352 // because no API found for this category. 1353 return CoreCategory 1354 1355 // https://docs.github.com/en/rest/search/search#search-code 1356 case strings.HasPrefix(path, "/search/code") && 1357 method == http.MethodGet: 1358 return CodeSearchCategory 1359 1360 case strings.HasPrefix(path, "/search/"): 1361 return SearchCategory 1362 case path == "/graphql": 1363 return GraphqlCategory 1364 case strings.HasPrefix(path, "/app-manifests/") && 1365 strings.HasSuffix(path, "/conversions") && 1366 method == http.MethodPost: 1367 return IntegrationManifestCategory 1368 1369 // https://docs.github.com/rest/migrations/source-imports#start-an-import 1370 case strings.HasPrefix(path, "/repos/") && 1371 strings.HasSuffix(path, "/import") && 1372 method == http.MethodPut: 1373 return SourceImportCategory 1374 1375 // https://docs.github.com/rest/code-scanning#upload-an-analysis-as-sarif-data 1376 case strings.HasSuffix(path, "/code-scanning/sarifs"): 1377 return CodeScanningUploadCategory 1378 1379 // https://docs.github.com/enterprise-cloud@latest/rest/scim 1380 case strings.HasPrefix(path, "/scim/"): 1381 return ScimCategory 1382 1383 // https://docs.github.com/en/rest/dependency-graph/dependency-submission#create-a-snapshot-of-dependencies-for-a-repository 1384 case strings.HasPrefix(path, "/repos/") && 1385 strings.HasSuffix(path, "/dependency-graph/snapshots") && 1386 method == http.MethodPost: 1387 return DependencySnapshotsCategory 1388 1389 // https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/orgs?apiVersion=2022-11-28#get-the-audit-log-for-an-organization 1390 case strings.HasSuffix(path, "/audit-log"): 1391 return AuditLogCategory 1392 } 1393 } 1394 1395 // RateLimits returns the rate limits for the current client. 1396 // 1397 // Deprecated: Use RateLimitService.Get instead. 1398 func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) { 1399 return c.RateLimit.Get(ctx) 1400 } 1401 1402 func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request { 1403 // To set extra headers, we must make a copy of the Request so 1404 // that we don't modify the Request we were given. This is required by the 1405 // specification of http.RoundTripper. 1406 // 1407 // Since we are going to modify only req.Header here, we only need a deep copy 1408 // of req.Header. 1409 convertedRequest := new(http.Request) 1410 *convertedRequest = *req 1411 convertedRequest.Header = make(http.Header, len(req.Header)) 1412 1413 for k, s := range req.Header { 1414 convertedRequest.Header[k] = append([]string(nil), s...) 1415 } 1416 convertedRequest.SetBasicAuth(id, secret) 1417 return convertedRequest 1418 } 1419 1420 /* 1421 UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls 1422 that need to use a higher rate limit associated with your OAuth application. 1423 1424 t := &github.UnauthenticatedRateLimitedTransport{ 1425 ClientID: "your app's client ID", 1426 ClientSecret: "your app's client secret", 1427 } 1428 client := github.NewClient(t.Client()) 1429 1430 This will add the client id and secret as a base64-encoded string in the format 1431 ClientID:ClientSecret and apply it as an "Authorization": "Basic" header. 1432 1433 See https://docs.github.com/rest/#unauthenticated-rate-limited-requests for 1434 more information. 1435 */ 1436 type UnauthenticatedRateLimitedTransport struct { 1437 // ClientID is the GitHub OAuth client ID of the current application, which 1438 // can be found by selecting its entry in the list at 1439 // https://github.com/settings/applications. 1440 ClientID string 1441 1442 // ClientSecret is the GitHub OAuth client secret of the current 1443 // application. 1444 ClientSecret string 1445 1446 // Transport is the underlying HTTP transport to use when making requests. 1447 // It will default to http.DefaultTransport if nil. 1448 Transport http.RoundTripper 1449 } 1450 1451 // RoundTrip implements the RoundTripper interface. 1452 func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { 1453 if t.ClientID == "" { 1454 return nil, errors.New("t.ClientID is empty") 1455 } 1456 if t.ClientSecret == "" { 1457 return nil, errors.New("t.ClientSecret is empty") 1458 } 1459 1460 req2 := setCredentialsAsHeaders(req, t.ClientID, t.ClientSecret) 1461 // Make the HTTP request. 1462 return t.transport().RoundTrip(req2) 1463 } 1464 1465 // Client returns an *http.Client that makes requests which are subject to the 1466 // rate limit of your OAuth application. 1467 func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client { 1468 return &http.Client{Transport: t} 1469 } 1470 1471 func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper { 1472 if t.Transport != nil { 1473 return t.Transport 1474 } 1475 return http.DefaultTransport 1476 } 1477 1478 // BasicAuthTransport is an http.RoundTripper that authenticates all requests 1479 // using HTTP Basic Authentication with the provided username and password. It 1480 // additionally supports users who have two-factor authentication enabled on 1481 // their GitHub account. 1482 type BasicAuthTransport struct { 1483 Username string // GitHub username 1484 Password string // GitHub password 1485 OTP string // one-time password for users with two-factor auth enabled 1486 1487 // Transport is the underlying HTTP transport to use when making requests. 1488 // It will default to http.DefaultTransport if nil. 1489 Transport http.RoundTripper 1490 } 1491 1492 // RoundTrip implements the RoundTripper interface. 1493 func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { 1494 req2 := setCredentialsAsHeaders(req, t.Username, t.Password) 1495 if t.OTP != "" { 1496 req2.Header.Set(headerOTP, t.OTP) 1497 } 1498 return t.transport().RoundTrip(req2) 1499 } 1500 1501 // Client returns an *http.Client that makes requests that are authenticated 1502 // using HTTP Basic Authentication. 1503 func (t *BasicAuthTransport) Client() *http.Client { 1504 return &http.Client{Transport: t} 1505 } 1506 1507 func (t *BasicAuthTransport) transport() http.RoundTripper { 1508 if t.Transport != nil { 1509 return t.Transport 1510 } 1511 return http.DefaultTransport 1512 } 1513 1514 // formatRateReset formats d to look like "[rate reset in 2s]" or 1515 // "[rate reset in 87m02s]" for the positive durations. And like "[rate limit was reset 87m02s ago]" 1516 // for the negative cases. 1517 func formatRateReset(d time.Duration) string { 1518 isNegative := d < 0 1519 if isNegative { 1520 d *= -1 1521 } 1522 secondsTotal := int(0.5 + d.Seconds()) 1523 minutes := secondsTotal / 60 1524 seconds := secondsTotal - minutes*60 1525 1526 var timeString string 1527 if minutes > 0 { 1528 timeString = fmt.Sprintf("%dm%02ds", minutes, seconds) 1529 } else { 1530 timeString = fmt.Sprintf("%ds", seconds) 1531 } 1532 1533 if isNegative { 1534 return fmt.Sprintf("[rate limit was reset %v ago]", timeString) 1535 } 1536 return fmt.Sprintf("[rate reset in %v]", timeString) 1537 } 1538 1539 func sleepUntilResetWithBuffer(ctx context.Context, reset time.Time) error { 1540 buffer := time.Second 1541 timer := time.NewTimer(time.Until(reset) + buffer) 1542 select { 1543 case <-ctx.Done(): 1544 if !timer.Stop() { 1545 <-timer.C 1546 } 1547 return ctx.Err() 1548 case <-timer.C: 1549 } 1550 return nil 1551 } 1552 1553 // When using roundTripWithOptionalFollowRedirect, note that it 1554 // is the responsibility of the caller to close the response body. 1555 func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, maxRedirects int, opts ...RequestOption) (*http.Response, error) { 1556 req, err := c.NewRequest("GET", u, nil, opts...) 1557 if err != nil { 1558 return nil, err 1559 } 1560 1561 var resp *http.Response 1562 // Use http.DefaultTransport if no custom Transport is configured 1563 req = withContext(ctx, req) 1564 if c.client.Transport == nil { 1565 resp, err = http.DefaultTransport.RoundTrip(req) 1566 } else { 1567 resp, err = c.client.Transport.RoundTrip(req) 1568 } 1569 if err != nil { 1570 return nil, err 1571 } 1572 1573 // If redirect response is returned, follow it 1574 if maxRedirects > 0 && resp.StatusCode == http.StatusMovedPermanently { 1575 _ = resp.Body.Close() 1576 u = resp.Header.Get("Location") 1577 resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, maxRedirects-1, opts...) 1578 } 1579 return resp, err 1580 } 1581 1582 // Bool is a helper routine that allocates a new bool value 1583 // to store v and returns a pointer to it. 1584 func Bool(v bool) *bool { return &v } 1585 1586 // Int is a helper routine that allocates a new int value 1587 // to store v and returns a pointer to it. 1588 func Int(v int) *int { return &v } 1589 1590 // Int64 is a helper routine that allocates a new int64 value 1591 // to store v and returns a pointer to it. 1592 func Int64(v int64) *int64 { return &v } 1593 1594 // String is a helper routine that allocates a new string value 1595 // to store v and returns a pointer to it. 1596 func String(v string) *string { return &v } 1597 1598 // roundTripperFunc creates a RoundTripper (transport) 1599 type roundTripperFunc func(*http.Request) (*http.Response, error) 1600 1601 func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { 1602 return fn(r) 1603 }