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

  1. At the handler, distinguish 'no/invalid token' (401) from 'authenticated but not allowed' (403) rather than collapsing both into ErrUnauthorized.
  2. Refresh tokens proactively before expiry to avoid hitting this path.
  3. Use errors.Is(err, ErrUnauthorized) consistently and map to the correct status code.
  4. 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

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

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/effe57ee9d9693a2. Report an issue: GitHub.