affaan-m/ECC · warning · ErrUnauthorized
unauthorized
Error message
unauthorized
What it means
A Go sentinel error ErrUnauthorized = errors.New("unauthorized") from the error-handling skill's domain package. It represents authentication or authorization failure at the domain level and is intended to be matched with errors.Is at the handler layer to map to HTTP 401/403.
Source
Thrown at skills/error-handling/SKILL.md:266
return JSONResponse(
status_code=500,
content={"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}},
)
```
## Go
### Sentinel Errors and Error Wrapping
```go
package domain
import "errors"
// Sentinel errors for type-checking
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
// Wrap errors with context — never lose the original
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
if err != nil {
return nil, fmt.Errorf("querying user %s: %w", id, err)
}
return user, nil
}
// At the handler level, unwrap to determine response
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.GetUser(r.Context(), chi.URLParam(r, "id"))View on GitHub (pinned to 01e15490f0)
Solutions
- At the handler, distinguish 'no/invalid token' (401) from 'authenticated but not allowed' (403) rather than collapsing both into ErrUnauthorized.
- Refresh tokens proactively before expiry to avoid hitting this path.
- Use errors.Is(err, ErrUnauthorized) consistently and map to the correct status code.
- Do not echo which specific check failed in the response body; return a generic message to avoid account enumeration.
Example fix
// before
var ErrUnauthorized = errors.New("unauthorized")
// after: split auth from authz for correct status codes
var (
ErrUnauthenticated = errors.New("unauthenticated") // -> 401
ErrForbidden = errors.New("forbidden") // -> 403
) Defensive patterns
Strategy: try-catch
Validate before calling
// ensure a valid authenticated principal before invoking the service
principal, ok := auth.FromContext(ctx)
if !ok {
return httpError(401, "unauthenticated")
} Type guard
null
Try / catch
if err := svc.Do(ctx, action); err != nil {
if errors.Is(err, domain.ErrUnauthorized) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
http.Error(w, "internal", http.StatusInternalServerError)
} Prevention
- Authenticate in middleware, not in business logic.
- Split ErrUnauthenticated (401) from ErrForbidden (403).
- Return generic messages to avoid enumeration.
When it happens
Trigger: A service method wraps ErrUnauthorized when a credentials check fails, a session token is invalid/expired, or an authenticated user lacks permission for a resource. The handler unwraps and decides between 401 (no auth) and 403 (forbidden).
Common situations: Expired JWT; missing or malformed Authorization header; user role insufficient for the action; token revocation list matched; session cleared on password change.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/effe57ee9d9693a2.
Report an issue: GitHub.