generated from cloudoperators/repository-template
-
Notifications
You must be signed in to change notification settings - Fork 1
chore(refactor): Error Refactoring Guidelines 8000 (for Issues) #697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dorneanu
wants to merge
11
commits into
main
Choose a base branch
from
dorneanu/issue-648/error_handling_guidelines
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
So, a quick recap: Error Types1. Raw Database Errors
2. Application Errorstype Error struct {
// Code is one of the error codes defined (e.g., NotFound, AlreadyExists)
Code Code
// Entity is the type of entity being operated on (e.g., "Issue")
Entity string
// ID is the ID of the entity
ID string
// Op is the operation being performed (e.
8000
g., "issueHandler.GetIssue")
Op string
// Message is a human-readable description of the error
Message string
// Err is the underlying error that triggered this one
Err error
// Fields contains additional structured metadata about the error
Fields map[string]interface{}
} 3. API-Specific Errors (GraphQL Example)type GraphQLError struct {
Message string `json:"message"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
} Database Layer (Infrastructure)Principle: Return raw errors without translation. Example Implementation// Database interface method
func (s *SqlDatabase) AddComponentVersionToIssue(issueId int64, componentVersionId int64) error {
// Check if issue exists
_, err := s.GetIssue(issueId)
if err != nil {
return err // Pass through the raw error
}
query := `
INSERT INTO ComponentVersionIssue (
componentversionissue_issue_id,
componentversionissue_component_version_id
) VALUES (
:issue_id,
:component_version_id
)
`
args := map[string]interface{}{
"issue_id": issueId,
"component_version_id": componentVersionId,
}
_, err = performExec(s, query, args, l)
return err // Return raw error
} Key Points
Application Layer (Business Logic)Principle: Translate database errors to rich application errors with context. Error Translation Function// FromDatabaseError translates database errors to application errors
func FromDatabaseError(op string, entity string, id string, err error) error {
if err == nil {
return nil
}
// Check for no rows error
if errors.Is(err, sql.ErrNoRows) {
return appErrors.NotFoundError(op, entity, id)
}
// Check for duplicate entry error
var dupErr *database.DuplicateEntryDatabaseError
if errors.As(err, &dupErr) {
return appErrors.AlreadyExistsError(op, entity, id)
}
// Check for specific MySQL/MariaDB errors
if strings.Contains(err.Error(), "Error 1062") || // Duplicate entry
strings.Contains(err.Error(), "Duplicate entry") {
return appErrors.AlreadyExistsError(op, entity, id)
}
// Check for foreign key constraint violations
if strings.Contains(strings.ToLower(err.Error()), "foreign key constraint") {
return appErrors.InvalidArgumentError(op, entity, "invalid reference")
}
// Default to internal error
return appErrors.InternalError(op, entity, id, err)
} Usage in Application Handlerfunc (is *issueHandler) AddComponentVersionToIssue(issueId, componentVersionId int64) (*entity.Issue, error) {
const op = "issueHandler.AddComponentVersionToIssue"
fields := logrus.Fields{
"event": AddComponentVersionToIssueEventName,
"id": issueId,
"componentVersionId": componentVersionId,
}
// Input validation
if issueId <= 0 {
err := appErrors.InvalidArgumentError(op, "Issue", "issue id must be positive")
is.logError(err, fields)
return nil, err
}
if componentVersionId <= 0 {
err := appErrors.InvalidArgumentError(op, "ComponentVersion", "component version id must be positive")
is.logError(err, fields)
return nil, err
}
// Check if issue exists
_, err := is.GetIssue(issueId)
if err != nil {
// Error already logged in GetIssue
return nil, err
}
err = is.database.AddComponentVersionToIssue(issueId, componentVersionId)
if err != nil {
entityID := fmt.Sprintf("issue %d, component version %d", issueId, componentVersionId)
appErr := shared.FromDatabaseError(op, "ComponentVersionIssue", entityID, err)
is.logError(appErr, fields)
return nil, appErr
}
is.eventRegistry.PushEvent(&AddComponentVersionToIssueEvent{
IssueID: issueId,
ComponentVersionID: componentVersionId,
})
return is.GetIssue(issueId)
} Key Points
API Layer (Presentation)Principle: Pass through application errors to be translated to API-specific format. Example Resolverfunc (r *mutationResolver) AddComponentVersionToIssue(ctx context.Context, issueID string, componentVersionID string) (*model.Issue, error) {
// Parse input IDs
issueIdInt, err := baseResolver.ParseCursor(&issueID)
if err != nil {
return nil, err // Just pass through the error
}
componentVersionIdInt, err := baseResolver.ParseCursor(&componentVersionID)
if err != nil {
return nil, err // Just pass through the error
}
// Call application layer
issue, err := r.App.AddComponentVersionToIssue(*issueIdInt, *componentVersionIdInt)
if err != nil {
return nil, err // Just pass through the error
}
// Transform to API model
i := model.NewIssue(issue)
return &i, nil
} Error Presenter (for GraphQL)func ErrorPresenter(ctx context.Context, err error) *gqlerror.Error {
// Try to extract application error
var appErr *appErrors.Error
if errors.As(err, &appErr) {
// Map the error code to a GraphQL error code
code := mapErrorCodeToGraphQL(appErr.Code)
// Create the GraphQL error with appropriate metadata
gqlErr := &gqlerror.Error{
Message: formatErrorMessage(appErr),
Extensions: map[string]interface{}{
"code": code,
},
}
// Add entity information if available
if appErr.Entity != "" {
gqlErr.Extensions["entity"] = appErr.Entity
}
// Add ID information if available
if appErr.ID != "" {
gqlErr.Extensions["id"] = appErr.ID
}
// Add fields if they exist and this is not an internal error
if appErr.Code != appErrors.Internal && appErr.Fields != nil && len(appErr.Fields) > 0 {
gqlErr.Extensions["fields"] = appErr.Fields
}
return gqlErr
}
// For any other error, use the default behavior
return graphql.DefaultErrorPresenter(ctx, err)
} Error Code Mappingfunc mapErrorCodeToGraphQL(code appErrors.Code) string {
switch code {
case appErrors.InvalidArgument:
return "BAD_REQUEST"
case appErrors.NotFound:
return "NOT_FOUND"
case appErrors.AlreadyExists:
return "CONFLICT"
case appErrors.PermissionDenied:
return "FORBIDDEN"
case appErrors.Unauthenticated:
return "UNAUTHENTICATED"
case appErrors.ResourceExhausted:
return "TOO_MANY_REQUESTS"
case appErrors.FailedPrecondition:
return "PRECONDITION_FAILED"
default:
return "INTERNAL"
}
} Key Points
|
This is how it could look like: Complete Error Flow ExampleScenario: Adding a ComponentVersion to a non-existent Issue1. Database Layer (Raw Error)// In SqlDatabase.AddComponentVersionToIssue
_, err := s.GetIssue(issueId)
if err != nil {
return err // Returns "record not found" error
} 2. Application Layer (Application Error)// In issueHandler.AddComponentVersionToIssue
err = is.database.AddComponentVersionToIssue(issueId, componentVersionId)
if err != nil {
entityID := fmt.Sprintf("issue %d, component version %d", issueId, componentVersionId)
appErr := shared.FromDatabaseError(op, "ComponentVersionIssue", entityID, err)
// "record not found" gets translated to NotFoundError
is.logError(appErr, fields)
return nil, appErr // Returns application error
} 3. API Layer (GraphQL Error)// In mutationResolver.AddComponentVersionToIssue
issue, err := r.App.AddComponentVersionToIssue(*issueIdInt, *componentVersionIdInt)
if err != nil {
return nil, err // Passes through the application error
} 4. Error Presenter// In ErrorPresenter
var appErr *appErrors.Error
if errors.As(err, &appErr) {
// NotFoundError gets mapped to "NOT_FOUND" code and formatted message
return &gqlerror.Error{
Message: "ComponentVersionIssue with ID 'issue 123, component version 456' not found",
Extensions: map[string]interface{}{
"code": "NOT_FOUND",
"entity": "ComponentVersionIssue",
"id": "issue 123, component version 456",
},
}
} 5. API Response{
"errors": [
{
"message": "ComponentVersionIssue with ID 'issue 123, component version 456' not found",
"extensions": {
"code": "NOT_FOUND",
"entity": "ComponentVersionIssue",
"id": "issue 123, component version 456"
}
}
],
"data": {
"addComponentVersionToIssue": null
}
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Description
Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.
What type of PR is this? (check all applicable)
Related Tickets & Documents
Added tests?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
Added to documentation?
Checklist