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