d95c62bad4
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
29 KiB
29 KiB
Error Handling Flow Diagram
Error Handling Architecture
┌──────────────────────────────────────────────────────────────┐
│ Error Handling Architecture │
└──────────────────────────────────────────────────────────────┘
Error Types:
├── Domain Errors Application-level business logic errors
├── Validation Errors Input validation failures
├── System Errors Infrastructure/system failures
└── Panic Recovery Runtime panic handling
Domain Error Structure
┌──────────────────────────────────────────────────────────────┐
│ DomainError (internal/handlers/errors.go) │
├──────────────────────────────────────────────────────────────┤
│ type DomainError struct { │
│ Code ErrorCode // Enum error code │
│ Message string // Human-readable message │
│ Err error // Underlying error (if any) │
│ StatusCode int // HTTP status code │
│ Field string // Field that caused error │
│ } │
│ │
│ func (e *DomainError) Error() string │
│ func (e *DomainError) Unwrap() error │
│ func (e *DomainError) WithField(field string) *DomainError │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Error Codes │
├──────────────────────────────────────────────────────────────┤
│ type ErrorCode string │
│ │
│ const ( │
│ // Input Validation (400) │
│ ErrCodeInvalidLanguage = "INVALID_LANGUAGE" │
│ ErrCodeInvalidLength = "INVALID_LENGTH" │
│ ErrCodeInvalidIcons = "INVALID_ICONS" │
│ ErrCodeInvalidTheme = "INVALID_THEME" │
│ ErrCodeInvalidVersion = "INVALID_VERSION" │
│ ErrCodeValidationFailed = "VALIDATION_FAILED" │
│ │
│ // Resource Errors (404, 500) │
│ ErrCodeDataNotFound = "DATA_NOT_FOUND" │
│ ErrCodeTemplateNotFound = "TEMPLATE_NOT_FOUND" │
│ ErrCodeTemplateError = "TEMPLATE_ERROR" │
│ │
│ // Processing Errors (500) │
│ ErrCodePDFGeneration = "PDF_GENERATION" │
│ ErrCodeInternalError = "INTERNAL_ERROR" │
│ │
│ // Rate Limiting (429) │
│ ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED" │
│ │
│ // Security (403) │
│ ErrCodeOriginMismatch = "ORIGIN_MISMATCH" │
│ ) │
└──────────────────────────────────────────────────────────────┘
Error Flow Patterns
Pattern 1: Validation Error
┌────────────────────────────────────────────────────────────┐
│ Validation Error Flow │
└────────────────────────────────────────────────────────────┘
Request: GET /?lang=xx
│
▼
┌─────────────────────────┐
│ Handler.Home() │
│ (cv_pages.go) │
└─────────────────────────┘
│
├─→ lang := r.URL.Query().Get("lang")
│ // lang = "xx"
│
├─→ err := validateLanguage(lang)
│ // "xx" not in ["en", "es"]
│
▼
┌─────────────────────────────────────────────────────────────┐
│ validateLanguage(lang) │
│ (cv_helpers.go) │
│ │
│ if lang != "en" && lang != "es" { │
│ return InvalidLanguageError(lang) │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ InvalidLanguageError(lang) │
│ (errors.go) │
│ │
│ return NewDomainError( │
│ ErrCodeInvalidLanguage, │
│ fmt.Sprintf("Unsupported language: %s", lang), │
│ http.StatusBadRequest, │
│ ).WithField("lang") │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Handler receives error │
│ │
│ if err != nil { │
│ h.HandleError(w, r, err) │
│ return │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ HandleError(w, r, err) │
│ (errors.go) │
│ │
│ 1. Cast to DomainError │
│ domErr, ok := err.(*DomainError) │
│ │
│ 2. Log error │
│ log.Printf("[ERROR] %s: %s", domErr.Code, domErr.Message) │
│ │
│ 3. Build response │
│ response := NewErrorResponse( │
│ string(domErr.Code), │
│ domErr.Message, │
│ ) │
│ response.Error.Field = domErr.Field │
│ │
│ 4. Send JSON error │
│ w.Header().Set("Content-Type", "application/json") │
│ w.WriteHeader(domErr.StatusCode) │
│ json.NewEncoder(w).Encode(response) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Client Response │
│ │
│ HTTP/1.1 400 Bad Request │
│ Content-Type: application/json │
│ │
│ { │
│ "success": false, │
│ "error": { │
│ "code": "INVALID_LANGUAGE", │
│ "message": "Unsupported language: xx (use 'en' or 'es')", │
│ "field": "lang" │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
Pattern 2: Data Loading Error
┌────────────────────────────────────────────────────────────┐
│ Data Loading Error Flow │
└────────────────────────────────────────────────────────────┘
Handler calls LoadCV("es")
│
▼
┌─────────────────────────────────────────────────────────────┐
│ cvmodel.LoadCV(lang) │
│ (internal/models/cv/cv.go) │
│ │
│ 1. Build file path │
│ filePath := fmt.Sprintf("data/cv-%s.json", lang) │
│ │
│ 2. Read file │
│ data, err := os.ReadFile(filePath) │
│ if err != nil { │
│ return nil, fmt.Errorf("failed to read CV: %w", err) │
│ } │
│ │
│ 3. Parse JSON │
│ var cv CV │
│ err = json.Unmarshal(data, &cv) │
│ if err != nil { │
│ return nil, fmt.Errorf("failed to parse CV: %w", err) │
│ } │
│ │
│ 4. Validate │
│ if err := cv.Validate(); err != nil { │
│ return nil, fmt.Errorf("invalid CV data: %w", err) │
│ } │
│ │
│ 5. Return │
│ return &cv, nil │
└─────────────────────────────────────────────────────────────┘
│
│ Error Case: File not found
▼
┌─────────────────────────────────────────────────────────────┐
│ Handler receives error │
│ │
│ cv, err := cvmodel.LoadCV(lang) │
│ if err != nil { │
│ // Wrap in DomainError │
│ domErr := DataNotFoundError("CV", lang) │
│ domErr.Err = err │
│ h.HandleError(w, r, domErr) │
│ return │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Client Response │
│ │
│ HTTP/1.1 500 Internal Server Error │
│ Content-Type: application/json │
│ │
│ { │
│ "success": false, │
│ "error": { │
│ "code": "DATA_NOT_FOUND", │
│ "message": "CV data not found for language: es" │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
Pattern 3: PDF Generation Error
┌────────────────────────────────────────────────────────────┐
│ PDF Generation Error Flow │
└────────────────────────────────────────────────────────────┘
Handler calls GeneratePDF()
│
▼
┌─────────────────────────────────────────────────────────────┐
│ pdf.GeneratePDF(htmlContent, options) │
│ (internal/pdf/generator.go) │
│ │
│ 1. Create context │
│ ctx, cancel := chromedp.NewContext(...) │
│ defer cancel() │
│ │
│ 2. Launch Chrome │
│ if err := chromedp.Run(ctx, ...); err != nil { │
│ return nil, fmt.Errorf("chrome launch: %w", err) │
│ } │
│ │
│ 3. Navigate and render │
│ err := chromedp.Run(ctx, │
│ chromedp.Navigate(dataURL), │
│ chromedp.WaitReady("body"), │
│ chromedp.PrintToPDF(&pdfBytes), │
│ ) │
│ if err != nil { │
│ return nil, fmt.Errorf("PDF generation: %w", err) │
│ } │
│ │
│ 4. Return PDF bytes │
│ return pdfBytes, nil │
└─────────────────────────────────────────────────────────────┘
│
│ Error Case: Chrome failed
▼
┌─────────────────────────────────────────────────────────────┐
│ Handler.ExportPDF receives error │
│ (cv_pdf.go) │
│ │
│ pdfBytes, err := pdf.GeneratePDF(htmlContent, opts) │
│ if err != nil { │
│ domErr := PDFGenerationError(err) │
│ h.HandleError(w, r, domErr) │
│ return │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Client Response │
│ │
│ HTTP/1.1 500 Internal Server Error │
│ Content-Type: application/json │
│ │
│ { │
│ "success": false, │
│ "error": { │
│ "code": "PDF_GENERATION", │
│ "message": "Failed to generate PDF. Please try again." │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
Pattern 4: Panic Recovery
┌────────────────────────────────────────────────────────────┐
│ Panic Recovery Flow │
└────────────────────────────────────────────────────────────┘
Request enters system
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Recovery Middleware │
│ (internal/middleware/recovery.go) │
│ │
│ func Recovery(next http.Handler) http.Handler { │
│ return http.HandlerFunc(func(w, r) { │
│ defer func() { │
│ if err := recover(); err != nil { │
│ // Capture panic │
│ stack := debug.Stack() │
│ │
│ // Log with stack trace │
│ log.Printf("PANIC: %v\n%s", err, stack) │
│ │
│ // Send error response │
│ http.Error(w, │
│ "Internal Server Error", │
│ http.StatusInternalServerError) │
│ } │
│ }() │
│ │
│ // Continue to next handler │
│ next.ServeHTTP(w, r) │
│ }) │
│ } │
└─────────────────────────────────────────────────────────────┘
│
│ Normal flow: no panic
├──────────────────────────────────┐
▼ │ Panic occurs
Handler executes ▼
│ ┌─────────────────────────────────┐
│ │ panic("something went wrong") │
│ └─────────────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────┐
│ │ defer recover() catches it │
│ │ ├─ Get stack trace │
│ │ ├─ Log error + stack │
│ │ └─ Send 500 response │
│ └─────────────────────────────────┘
▼ │
Response sent ▼
┌─────────────────────────────────┐
│ Client receives 500 │
└─────────────────────────────────┘
Error Response Formats
┌────────────────────────────────────────────────────────────┐
│ Error Response Formats │
└────────────────────────────────────────────────────────────┘
Standard API Error (JSON):
{
"success": false,
"error": {
"code": "INVALID_LANGUAGE",
"message": "Unsupported language: xx (use 'en' or 'es')",
"field": "lang"
}
}
Validation Error with Multiple Fields:
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"fields": {
"lang": "Invalid language",
"length": "Invalid length"
}
}
}
Internal Error (Generic):
{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again."
}
}
HTML Error Page (for page requests):
<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body>
<h1>Oops! Something went wrong</h1>
<p>We're sorry, but we couldn't process your request.</p>
<p>Error: INVALID_LANGUAGE</p>
<a href="/">Go back home</a>
</body>
</html>
Error Logging
┌────────────────────────────────────────────────────────────┐
│ Error Logging │
└────────────────────────────────────────────────────────────┘
Log Format:
[ERROR] <ERROR_CODE>: <message>
[ERROR] Additional context: <details>
[ERROR] Stack trace (if panic):
<stack trace lines>
Examples:
[ERROR] INVALID_LANGUAGE: Unsupported language: xx (use 'en' or 'es')
[ERROR] Field: lang
[ERROR] PDF_GENERATION: Failed to generate PDF
[ERROR] Underlying error: chrome launch failed: context deadline exceeded
[ERROR] PANIC: runtime error: invalid memory address
[ERROR] Stack trace:
goroutine 23 [running]:
main.(*CVHandler).Home(...)
/app/internal/handlers/cv_pages.go:42
...
Error Handling Best Practices
┌────────────────────────────────────────────────────────────┐
│ Error Handling Best Practices │
└────────────────────────────────────────────────────────────┘
1. USE TYPED ERRORS
✓ return InvalidLanguageError(lang)
✗ return errors.New("invalid language")
2. WRAP ERRORS WITH CONTEXT
✓ return fmt.Errorf("failed to load CV: %w", err)
✗ return err
3. LOG BEFORE RESPONDING
✓ log.Printf("[ERROR] %s", err)
h.HandleError(w, r, err)
✗ h.HandleError(w, r, err) // No logging
4. USE APPROPRIATE STATUS CODES
✓ 400 for validation errors
404 for not found
429 for rate limiting
500 for server errors
✗ Always returning 500
5. DON'T LEAK INTERNAL DETAILS
✓ "Failed to generate PDF. Please try again."
✗ "chromedp: chrome crashed at line 42 in generator.go"
6. PROVIDE ACTIONABLE MESSAGES
✓ "Unsupported language: xx (use 'en' or 'es')"
✗ "Invalid input"
7. USE RECOVERY MIDDLEWARE
✓ Catch all panics at middleware level
✗ Let panics crash the server
8. INCLUDE FIELD INFORMATION
✓ error.WithField("lang")
✗ Generic error without field context
Related Diagrams
- Request Flow - HTTP request lifecycle
- Middleware Chain - Middleware execution
- Handler Organization - Handler structure