faf3a2ca45
Created detailed ASCII diagrams documenting the entire system architecture: 1. System Architecture (_go-learning/diagrams/01-system-architecture.md) - Overall system architecture with client/server/storage layers - Layered architecture (Presentation → Application → Business → Data) - Component interaction and HTTP request flow - Data flow from app start through per-request lifecycle - Package dependencies and file organization
493 lines
29 KiB
Markdown
493 lines
29 KiB
Markdown
# 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](./02-request-flow.md) - HTTP request lifecycle
|
|
- [Middleware Chain](./03-middleware-chain.md) - Middleware execution
|
|
- [Handler Organization](./04-handler-organization.md) - Handler structure
|