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