{"record":{"id":"effe57ee9d9693a2","repo":"affaan-m/ECC","slug":"unauthorized","errorCode":null,"errorMessage":"unauthorized","messagePattern":"unauthorized","errorType":"error_code","errorClass":"ErrUnauthorized","httpStatus":null,"severity":"warning","filePath":"skills/error-handling/SKILL.md","lineNumber":266,"sourceCode":"    return JSONResponse(\n        status_code=500,\n        content={\"error\": {\"code\": \"INTERNAL_ERROR\", \"message\": \"An unexpected error occurred\"}},\n    )\n```\n\n## Go\n\n### Sentinel Errors and Error Wrapping\n\n```go\npackage domain\n\nimport \"errors\"\n\n// Sentinel errors for type-checking\nvar (\n    ErrNotFound    = errors.New(\"not found\")\n    ErrUnauthorized = errors.New(\"unauthorized\")\n    ErrConflict     = errors.New(\"conflict\")\n)\n\n// Wrap errors with context — never lose the original\nfunc (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {\n    user, err := r.db.QueryRow(ctx, \"SELECT * FROM users WHERE id = $1\", id)\n    if errors.Is(err, sql.ErrNoRows) {\n        return nil, fmt.Errorf(\"user %s: %w\", id, ErrNotFound)\n    }\n    if err != nil {\n        return nil, fmt.Errorf(\"querying user %s: %w\", id, err)\n    }\n    return user, nil\n}\n\n// At the handler level, unwrap to determine response\nfunc (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {\n    user, err := h.service.GetUser(r.Context(), chi.URLParam(r, \"id\"))","sourceCodeStart":248,"sourceCodeEnd":284,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/skills/error-handling/SKILL.md#L248-L284","documentation":"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.","triggerScenarios":"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).","commonSituations":"Expired JWT; missing or malformed Authorization header; user role insufficient for the action; token revocation list matched; session cleared on password change.","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."],"exampleFix":"// before\nvar ErrUnauthorized = errors.New(\"unauthorized\")\n\n// after: split auth from authz for correct status codes\nvar (\n    ErrUnauthenticated = errors.New(\"unauthenticated\") // -> 401\n    ErrForbidden       = errors.New(\"forbidden\")        // -> 403\n)","handlingStrategy":"try-catch","validationCode":"// ensure a valid authenticated principal before invoking the service\nprincipal, ok := auth.FromContext(ctx)\nif !ok {\n    return httpError(401, \"unauthenticated\")\n}","typeGuard":"null","tryCatchPattern":"if err := svc.Do(ctx, action); err != nil {\n    if errors.Is(err, domain.ErrUnauthorized) {\n        http.Error(w, \"unauthorized\", http.StatusUnauthorized)\n        return\n    }\n    http.Error(w, \"internal\", http.StatusInternalServerError)\n}","preventionTips":["Authenticate in middleware, not in business logic.","Split ErrUnauthenticated (401) from ErrForbidden (403).","Return generic messages to avoid enumeration."],"tags":["go","sentinel-error","authentication","authorization"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}