chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/github/github-mcp-server/pkg/utils"
|
||||
"github.com/google/go-github/v89/github"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type GitHubAPIError struct {
|
||||
Message string `json:"message"`
|
||||
Response *github.Response `json:"-"`
|
||||
Err error `json:"-"`
|
||||
}
|
||||
|
||||
// NewGitHubAPIError creates a new GitHubAPIError with the provided message, response, and error.
|
||||
func newGitHubAPIError(message string, resp *github.Response, err error) *GitHubAPIError {
|
||||
return &GitHubAPIError{
|
||||
Message: message,
|
||||
Response: resp,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *GitHubAPIError) Error() string {
|
||||
return fmt.Errorf("%s: %w", e.Message, e.Err).Error()
|
||||
}
|
||||
|
||||
type GitHubGraphQLError struct {
|
||||
Message string `json:"message"`
|
||||
Err error `json:"-"`
|
||||
}
|
||||
|
||||
func newGitHubGraphQLError(message string, err error) *GitHubGraphQLError {
|
||||
return &GitHubGraphQLError{
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *GitHubGraphQLError) Error() string {
|
||||
return fmt.Errorf("%s: %w", e.Message, e.Err).Error()
|
||||
}
|
||||
|
||||
type GitHubRawAPIError struct {
|
||||
Message string `json:"message"`
|
||||
Response *http.Response `json:"-"`
|
||||
Err error `json:"-"`
|
||||
}
|
||||
|
||||
func newGitHubRawAPIError(message string, resp *http.Response, err error) *GitHubRawAPIError {
|
||||
return &GitHubRawAPIError{
|
||||
Message: message,
|
||||
Response: resp,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *GitHubRawAPIError) Error() string {
|
||||
return fmt.Errorf("%s: %w", e.Message, e.Err).Error()
|
||||
}
|
||||
|
||||
type GitHubErrorKey struct{}
|
||||
type GitHubCtxErrors struct {
|
||||
api []*GitHubAPIError
|
||||
graphQL []*GitHubGraphQLError
|
||||
raw []*GitHubRawAPIError
|
||||
}
|
||||
|
||||
// ContextWithGitHubErrors updates or creates a context with a pointer to GitHub error information (to be used by middleware).
|
||||
func ContextWithGitHubErrors(ctx context.Context) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
|
||||
// If the context already has GitHubCtxErrors, we just empty the slices to start fresh
|
||||
val.api = []*GitHubAPIError{}
|
||||
val.graphQL = []*GitHubGraphQLError{}
|
||||
val.raw = []*GitHubRawAPIError{}
|
||||
} else {
|
||||
// If not, we create a new GitHubCtxErrors and set it in the context
|
||||
ctx = context.WithValue(ctx, GitHubErrorKey{}, &GitHubCtxErrors{})
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// GetGitHubAPIErrors retrieves the slice of GitHubAPIErrors from the context.
|
||||
func GetGitHubAPIErrors(ctx context.Context) ([]*GitHubAPIError, error) {
|
||||
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
|
||||
return val.api, nil // return the slice of API errors from the context
|
||||
}
|
||||
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
|
||||
}
|
||||
|
||||
// GetGitHubGraphQLErrors retrieves the slice of GitHubGraphQLErrors from the context.
|
||||
func GetGitHubGraphQLErrors(ctx context.Context) ([]*GitHubGraphQLError, error) {
|
||||
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
|
||||
return val.graphQL, nil // return the slice of GraphQL errors from the context
|
||||
}
|
||||
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
|
||||
}
|
||||
|
||||
// GetGitHubRawAPIErrors retrieves the slice of GitHubRawAPIErrors from the context.
|
||||
func GetGitHubRawAPIErrors(ctx context.Context) ([]*GitHubRawAPIError, error) {
|
||||
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
|
||||
return val.raw, nil // return the slice of raw API errors from the context
|
||||
}
|
||||
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
|
||||
}
|
||||
|
||||
func NewGitHubAPIErrorToCtx(ctx context.Context, message string, resp *github.Response, err error) (context.Context, error) {
|
||||
apiErr := newGitHubAPIError(message, resp, err)
|
||||
if ctx != nil {
|
||||
_, _ = addGitHubAPIErrorToContext(ctx, apiErr) // Explicitly ignore error for graceful handling
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func NewGitHubGraphQLErrorToCtx(ctx context.Context, message string, err error) (context.Context, error) {
|
||||
graphQLErr := newGitHubGraphQLError(message, err)
|
||||
if ctx != nil {
|
||||
_, _ = addGitHubGraphQLErrorToContext(ctx, graphQLErr) // Explicitly ignore error for graceful handling
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func addGitHubAPIErrorToContext(ctx context.Context, err *GitHubAPIError) (context.Context, error) {
|
||||
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
|
||||
val.api = append(val.api, err) // append the error to the existing slice in the context
|
||||
return ctx, nil
|
||||
}
|
||||
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
|
||||
}
|
||||
|
||||
func addGitHubGraphQLErrorToContext(ctx context.Context, err *GitHubGraphQLError) (context.Context, error) {
|
||||
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
|
||||
val.graphQL = append(val.graphQL, err) // append the error to the existing slice in the context
|
||||
return ctx, nil
|
||||
}
|
||||
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
|
||||
}
|
||||
|
||||
func addRawAPIErrorToContext(ctx context.Context, err *GitHubRawAPIError) (context.Context, error) {
|
||||
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
|
||||
val.raw = append(val.raw, err)
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
|
||||
}
|
||||
|
||||
// NewGitHubAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
|
||||
func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github.Response, err error) *mcp.CallToolResult {
|
||||
apiErr := newGitHubAPIError(message, resp, err)
|
||||
if ctx != nil {
|
||||
_, _ = addGitHubAPIErrorToContext(ctx, apiErr) // Explicitly ignore error for graceful handling
|
||||
}
|
||||
|
||||
var rateLimitErr *github.RateLimitError
|
||||
if stderrors.As(err, &rateLimitErr) {
|
||||
resetTime := rateLimitErr.Rate.Reset.Time
|
||||
if !resetTime.IsZero() {
|
||||
retryIn := time.Until(resetTime).Round(time.Second)
|
||||
if retryIn > 0 {
|
||||
return utils.NewToolResultError(fmt.Sprintf(
|
||||
"%s: GitHub API rate limit exceeded. Retry after %v.", message, retryIn))
|
||||
}
|
||||
}
|
||||
return utils.NewToolResultError(fmt.Sprintf(
|
||||
"%s: GitHub API rate limit exceeded. Wait before retrying.", message))
|
||||
}
|
||||
|
||||
var abuseErr *github.AbuseRateLimitError
|
||||
if stderrors.As(err, &abuseErr) {
|
||||
if abuseErr.RetryAfter != nil {
|
||||
retryAfter := abuseErr.RetryAfter.Round(time.Second)
|
||||
if retryAfter > 0 {
|
||||
return utils.NewToolResultError(fmt.Sprintf(
|
||||
"%s: GitHub secondary rate limit exceeded. Retry after %v.",
|
||||
message, retryAfter))
|
||||
}
|
||||
}
|
||||
return utils.NewToolResultError(fmt.Sprintf(
|
||||
"%s: GitHub secondary rate limit exceeded. Wait before retrying.", message))
|
||||
}
|
||||
|
||||
return utils.NewToolResultErrorFromErr(message, err)
|
||||
}
|
||||
|
||||
// NewGitHubGraphQLErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
|
||||
func NewGitHubGraphQLErrorResponse(ctx context.Context, message string, err error) *mcp.CallToolResult {
|
||||
graphQLErr := newGitHubGraphQLError(message, err)
|
||||
if ctx != nil {
|
||||
_, _ = addGitHubGraphQLErrorToContext(ctx, graphQLErr) // Explicitly ignore error for graceful handling
|
||||
}
|
||||
return utils.NewToolResultErrorFromErr(message, err)
|
||||
}
|
||||
|
||||
// NewGitHubRawAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
|
||||
func NewGitHubRawAPIErrorResponse(ctx context.Context, message string, resp *http.Response, err error) *mcp.CallToolResult {
|
||||
rawErr := newGitHubRawAPIError(message, resp, err)
|
||||
if ctx != nil {
|
||||
_, _ = addRawAPIErrorToContext(ctx, rawErr) // Explicitly ignore error for graceful handling
|
||||
}
|
||||
return utils.NewToolResultErrorFromErr(message, err)
|
||||
}
|
||||
|
||||
// NewGitHubAPIStatusErrorResponse handles cases where the API call succeeds (err == nil)
|
||||
// but returns an unexpected HTTP status code. It creates a synthetic error from the
|
||||
// status code and response body, then records it in context for observability tracking.
|
||||
func NewGitHubAPIStatusErrorResponse(ctx context.Context, message string, resp *github.Response, body []byte) *mcp.CallToolResult {
|
||||
err := fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
|
||||
return NewGitHubAPIErrorResponse(ctx, message, resp, err)
|
||||
}
|
||||
|
||||
// StructuredResolutionError is a machine-readable error returned by name-resolution
|
||||
// helpers (e.g. resolving a project field or single-select option by name). Agents
|
||||
// can parse the JSON body to self-correct without re-prompting.
|
||||
//
|
||||
// Kind values:
|
||||
// - "field_not_found" — no project field matches the supplied name
|
||||
// - "field_ambiguous" — more than one project field shares the supplied name
|
||||
// - "option_not_found" — no option on the resolved single-select field matches
|
||||
// - "option_ambiguous" — duplicate option names on the resolved field
|
||||
// - "item_not_in_project" — the issue/PR exists but is not an item on the project
|
||||
// - "wrong_field_type" — the named field is not the data type the caller expected
|
||||
type StructuredResolutionError struct {
|
||||
Kind string `json:"error"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Field string `json:"field,omitempty"`
|
||||
Candidates []any `json:"candidates,omitempty"`
|
||||
Hint string `json:"hint,omitempty"`
|
||||
}
|
||||
|
||||
// Error implements the error interface; the message is the JSON body so that the
|
||||
// downstream tool result also carries the structured payload as plain text.
|
||||
func (e *StructuredResolutionError) Error() string {
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"error":%q,"name":%q}`, e.Kind, e.Name)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// NewStructuredResolutionError constructs a StructuredResolutionError.
|
||||
func NewStructuredResolutionError(kind, name, hint string, candidates []any) *StructuredResolutionError {
|
||||
return &StructuredResolutionError{
|
||||
Kind: kind,
|
||||
Name: name,
|
||||
Hint: hint,
|
||||
Candidates: candidates,
|
||||
}
|
||||
}
|
||||
|
||||
// NewStructuredResolutionErrorResponse returns an mcp.CallToolResult whose text body
|
||||
// is the JSON-serialised StructuredResolutionError, suitable for agent self-correction.
|
||||
func NewStructuredResolutionErrorResponse(err *StructuredResolutionError) *mcp.CallToolResult {
|
||||
return utils.NewToolResultError(err.Error())
|
||||
}
|
||||
Reference in New Issue
Block a user