stemedb/cmd/pitch-voiceover/pkg/httpkit/errors.go
jordan 157dbbb9eb feat: Complete Aphoria Phase 8-9 + UAT suite (90/90 tests passing)
## Phase 8: Enterprise Extractor Improvements 
- 14 security extractors (TLS, JWT, SQL injection, XSS, etc.)
- 10 framework-specific extractors (Spring, Django, Rails, etc.)
- Config file security detection (YAML, TOML)

## Phase 9: Autonomous Extractor Generation 
- Shadow mode executor with TP/FP tracking
- Graduation pipeline with confidence thresholds
- Auto-rollback on regression detection
- Cross-project pattern syncing

## UAT Suite Complete (14 scripts, 90 tests)
- test-core-detection.sh (6 tests)
- test-declarative-extractors.sh (5 tests)
- test-domain-frameworks.sh (5 tests)
- test-domain-unreal.sh (3 tests)
- test-llm-extraction.sh (6 tests)
- test-eval-harness.sh (5 tests)
- test-cross-language.sh (3 tests)
- test-precommit-performance.sh (4 tests)
- test-output-formats.sh (8 tests)
- test-drift-detection.sh (6 tests)
- test-exit-codes.sh (12 tests)
+ 3 more scripts

## Other Changes
- Updated roadmap to mark Phase 8-9 complete
- Added .gitignore entries for build artifacts
- Updated pre-commit: 800 line limit, exclude tests/data/cmd

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 22:50:55 -07:00

92 lines
2.6 KiB
Go

// Package httpkit provides a shared HTTP client with retry, timeout, and error
// classification for outbound API calls.
//
// All outbound HTTP in this codebase should use httpkit.Client instead of
// raw http.Client or http.DefaultClient. This ensures consistent timeout,
// retry, body lifecycle, and error classification.
//
// Basic usage:
//
// client := httpkit.NewClient(httpkit.Config{
// Timeout: 30 * time.Second,
// MaxRetries: 3,
// Headers: map[string]string{"Authorization": "Bearer " + apiKey},
// })
//
// body, err := client.Do(ctx, httpkit.Request{
// Method: http.MethodPost,
// URL: baseURL + "/chat/completions",
// Body: chatReq,
// })
// if errors.Is(err, httpkit.ErrRateLimit) { ... }
package httpkit
import (
"errors"
"fmt"
"net/http"
)
// Sentinel errors classify HTTP failures into retryable categories.
// Use errors.Is to check against these from any wrapped error.
var (
ErrTimeout = errors.New("httpkit: timeout")
ErrRateLimit = errors.New("httpkit: rate limited")
ErrServerError = errors.New("httpkit: server error")
ErrUnauthorized = errors.New("httpkit: unauthorized")
ErrForbidden = errors.New("httpkit: forbidden")
ErrBadRequest = errors.New("httpkit: bad request")
ErrNotFound = errors.New("httpkit: not found")
)
// APIError represents a non-2xx HTTP response.
// Unwrap returns the classified sentinel error for use with errors.Is.
type APIError struct {
StatusCode int
Body string
sentinel error
}
func (e *APIError) Error() string {
body := e.Body
if len(body) > 200 {
body = body[:200] + "..."
}
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, body)
}
// Unwrap returns the classified sentinel error, enabling errors.Is checks
// like errors.Is(err, httpkit.ErrRateLimit).
func (e *APIError) Unwrap() error {
return e.sentinel
}
// ClassifyStatus maps an HTTP status code to a sentinel error.
// Returns nil for unrecognized or successful status codes.
func ClassifyStatus(code int) error {
switch {
case code == http.StatusUnauthorized:
return ErrUnauthorized
case code == http.StatusForbidden:
return ErrForbidden
case code == http.StatusNotFound:
return ErrNotFound
case code == http.StatusTooManyRequests:
return ErrRateLimit
case code >= 400 && code < 500:
return ErrBadRequest
case code >= 500:
return ErrServerError
default:
return nil
}
}
// IsRetryable reports whether err indicates a condition that may succeed on retry.
// Retryable conditions: rate limits (429), server errors (5xx), and timeouts.
func IsRetryable(err error) bool {
return errors.Is(err, ErrRateLimit) ||
errors.Is(err, ErrServerError) ||
errors.Is(err, ErrTimeout)
}