package server import ( "errors" "net/http" "chat-agent-system/internal/auth" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/docgov" "chat-agent-system/internal/errcode" "chat-agent-system/internal/freedoc" "chat-agent-system/internal/httpx" "chat-agent-system/internal/maintenance" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/swboot" "chat-agent-system/internal/sysadmin" "chat-agent-system/internal/taskrun" ) // domainHTTPStatus is the single adapter that maps known domain sentinel / // typed errors to HTTP status codes. Callers that need a different default // (400 vs 500) must apply it when ok is false. // // This is the only production place in internal/server that should match // docgov.Err* (and closely related domain sentinels) for HTTP status selection. // handlers.go must call this mapper (or writeDocGovError / writeTaskError) // instead of branching on docgov.Err* directly. func domainHTTPStatus(err error) (status int, ok bool) { if err == nil { return 0, false } var busy *docgov.DocumentBusyError if errors.As(err, &busy) { return http.StatusConflict, true } var ce *docgov.ContentError if errors.As(err, &ce) { if ce.Code == docgov.ErrCodeDocumentNotFound { return http.StatusNotFound, true } return http.StatusConflict, true } switch { case errors.Is(err, docgov.ErrDocumentBusy), errors.Is(err, docgov.ErrDocumentLocked), errors.Is(err, docgov.ErrForceEditNotAllowed), errors.Is(err, docgov.ErrLeaseNotHeld), errors.Is(err, docgov.ErrLeaseExpired): return http.StatusConflict, true case errors.Is(err, docgov.ErrDocumentNotFound), errors.Is(err, docgov.ErrProposalNotFound): return http.StatusNotFound, true case errors.Is(err, docgov.ErrUnauthorizedActor), errors.Is(err, docgov.ErrOnlyAuthorCanEdit), errors.Is(err, docgov.ErrReviewerCannotEdit), errors.Is(err, docgov.ErrApproverCannotEdit): return http.StatusForbidden, true case errors.Is(err, projectworkflow.ErrPrerequisiteNotMet): return http.StatusBadRequest, true default: return 0, false } } func writeTaskError(w http.ResponseWriter, r *http.Request, err error, payload any) { status := http.StatusInternalServerError if mapped, ok := domainHTTPStatus(err); ok { status = mapped } if payload != nil { def := errcode.FromStatus(status) if d, found := errcode.FromError(err); found { def = d } noteCoded(w, r, status, def, err) writeJSONResponse(w, status, payload) return } writeDomainError(w, r, status, err) } func writeDocGovError(w http.ResponseWriter, r *http.Request, err error) { var busy *docgov.DocumentBusyError if errors.As(err, &busy) { def, ok := errcode.Lookup("DOC_BUSY") if !ok { def = errcode.FromStatus(http.StatusConflict) } noteCoded(w, r, http.StatusConflict, def, err) writeJSONResponse(w, http.StatusConflict, map[string]any{ "error": "document_busy", "detail": busy, }) return } status := http.StatusBadRequest if mapped, ok := domainHTTPStatus(err); ok { status = mapped } writeDomainError(w, r, status, err) } func noteCoded(w http.ResponseWriter, r *http.Request, status int, def errcode.Def, err error) { rid := incomingRequestID(w, r) if rid == "" { rid = httpx.RequestID(r) } if w != nil { w.Header().Set(httpx.HeaderRequestID, rid) w.Header().Set(httpx.HeaderErrorCode, def.ID) } legacy := "" if err != nil { legacy = err.Error() } reportHTTPError(r, status, def, rid, legacy) } func init() { bind := func(err error, id string) { errcode.Bind(err, id) } bind(docgov.ErrDocumentNotFound, "DOCUMENT_NOT_FOUND") bind(docgov.ErrDocumentLocked, "DOCUMENT_LOCKED") bind(docgov.ErrDocumentBusy, "DOC_BUSY") bind(docgov.ErrLeaseNotHeld, "LEASE_NOT_HELD") bind(docgov.ErrLeaseExpired, "LEASE_EXPIRED") bind(docgov.ErrStaleFencing, "LEASE_STALE") bind(docgov.ErrForceEditNotAllowed, "FORCE_EDIT_NOT_ALLOWED") bind(docgov.ErrProposalNotFound, "PROPOSAL_NOT_FOUND") bind(docgov.ErrUnauthorizedActor, "UNAUTHORIZED_ACTOR") bind(docgov.ErrOnlyAuthorCanEdit, "AUTHOR_ONLY_EDIT") bind(docgov.ErrReviewerCannotEdit, "REVIEWER_CANNOT_EDIT") bind(docgov.ErrApproverCannotEdit, "APPROVER_CANNOT_EDIT") bind(docgov.ErrInvalidStateTransition, "INVALID_STATE_TRANSITION") bind(docgov.ErrRejectReasonRequired, "REJECT_REASON_REQUIRED") bind(docgov.ErrProposalAlreadyDecided, "PROPOSAL_ALREADY_DECIDED") bind(docgov.ErrChangeProposalPending, "CHANGE_PROPOSAL_PENDING") bind(docgov.ErrContentProjectionMissing, "DOCUMENT_CONTENT_PROJECTION_MISSING") bind(docgov.ErrContentProjectionEmpty, "DOCUMENT_CONTENT_PROJECTION_EMPTY") bind(docgov.ErrContentIntegrityMismatch, "DOCUMENT_CONTENT_INTEGRITY_MISMATCH") bind(freedoc.ErrNotFound, "FREEDOC_NOT_FOUND") bind(freedoc.ErrForbidden, "FREEDOC_FORBIDDEN") bind(freedoc.ErrConflict, "FREEDOC_CONFLICT") bind(freedoc.ErrInvalidParam, "FREEDOC_INVALID_PARAM") bind(freedoc.ErrMessageNotExportable, "FREEDOC_NOT_EXPORTABLE") bind(auth.ErrUsernameExists, "AUTH_USERNAME_EXISTS") bind(sysadmin.ErrUnknownSetting, "SETTING_UNKNOWN") bind(sysadmin.ErrSettingReadOnly, "SETTING_READ_ONLY") bind(sysadmin.ErrInvalidSettingValue, "SETTING_INVALID_VALUE") bind(sysadmin.ErrStoreUnavailable, "SETTING_STORE_UNAVAILABLE") bind(sysadmin.ErrExpectedRevisionRequired, "SETTING_REVISION_REQUIRED") bind(sysadmin.ErrSettingRevisionConflict, "SETTING_REVISION_CONFLICT") bind(sysadmin.ErrSettingNotFound, "SETTING_NOT_FOUND") bind(projectworkflow.ErrUnauthenticated, "WORKFLOW_UNAUTHENTICATED") bind(projectworkflow.ErrForbiddenNotCreator, "WORKFLOW_FORBIDDEN_NOT_CREATOR") bind(projectworkflow.ErrProjectNotFound, "PROJECT_NOT_FOUND") bind(projectworkflow.ErrWorkflowNotFound, "WORKFLOW_NOT_FOUND") bind(projectworkflow.ErrConcurrentConflict, "WORKFLOW_CONCURRENT_CONFLICT") bind(projectworkflow.ErrPrerequisiteNotMet, "WORKFLOW_PREREQUISITE") bind(projectworkflow.ErrInvalidInput, "WORKFLOW_INVALID_INPUT") bind(projectworkflow.ErrDuplicateProjectName, "PROJECT_NAME_DUPLICATE") bind(projectworkflow.ErrLegacySwitchBlocked, "WORKFLOW_LEGACY_SWITCH_BLOCKED") bind(taskrun.ErrRunNotResumable, "TASKRUN_NOT_RESUMABLE") bind(taskrun.ErrRunAlreadyActive, "TASKRUN_ALREADY_ACTIVE") bind(taskrun.ErrTerminalConflict, "TASKRUN_TERMINAL_CONFLICT") bind(taskrun.ErrCleanupBlocked, "TASKRUN_CLEANUP_BLOCKED") bind(taskrun.ErrUnauthorizedBackend, "TASKRUN_UNAUTHORIZED_BACKEND") bind(taskrun.ErrSensitivePayload, "TASKRUN_SENSITIVE_PAYLOAD") bind(taskrun.ErrStoreNotAvailable, "TASKRUN_STORE_UNAVAILABLE") bind(taskrun.ErrIdempotencyConflict, "TASKRUN_IDEMPOTENCY_CONFLICT") bind(ctxmgr.ErrSessionOwnerMismatch, "SESSION_OWNER_MISMATCH") bind(ctxmgr.ErrChatTurnNotFound, "CHAT_TURN_NOT_FOUND") bind(ctxmgr.ErrIdempotencyKeyReused, "IDEMPOTENCY_KEY_REUSED") bind(maintenance.ErrNotFound, "MAINTENANCE_NOT_FOUND") bind(maintenance.ErrConflictOpen, "MAINTENANCE_CONFLICT_OPEN") bind(maintenance.ErrInvalidWindow, "MAINTENANCE_INVALID_WINDOW") bind(maintenance.ErrBadStatus, "MAINTENANCE_BAD_STATUS") bind(swboot.ErrMergeRequiresSeparateAuthorization, "SWBOOT_MERGE_FORBIDDEN") bind(swboot.ErrRepoPathNotAllowlisted, "SWBOOT_REPO_NOT_ALLOWLISTED") bind(swboot.ErrControlledPatchDisabled, "SWBOOT_PATCH_DISABLED") bind(swboot.ErrPatchExecutionUnimplemented, "SWBOOT_PATCH_UNIMPLEMENTED") bind(projectinstance.ErrNotAvailable, "INSTANCE_NOT_AVAILABLE") bind(projectinstance.ErrForbiddenNotCreator, "INSTANCE_FORBIDDEN_NOT_CREATOR") bind(projectinstance.ErrPlanFrozen, "INSTANCE_PLAN_FROZEN") bind(projectinstance.ErrWorkflowSwitchFrozen, "INSTANCE_SWITCH_FROZEN") bind(projectinstance.ErrWorkflowAlreadyActive, "INSTANCE_ALREADY_ACTIVE") bind(projectinstance.ErrPlanChangedReconfirm, "INSTANCE_PLAN_RECONFIRM") bind(projectinstance.ErrValidationFailed, "INSTANCE_VALIDATION_FAILED") bind(projectinstance.ErrTokenInvalid, "INSTANCE_ACTIVATION_INVALID") bind(projectinstance.ErrConcurrentConflict, "INSTANCE_CONFLICT") bind(projectinstance.ErrPlanRevisionConflict, "INSTANCE_PLAN_CONFLICT") bind(projectinstance.ErrInstanceNotFound, "INSTANCE_NOT_FOUND") bind(projectinstance.ErrStageGateNotReady, "INSTANCE_STAGE_GATE") bind(projectinstance.ErrStageNotActive, "INSTANCE_STAGE_NOT_ACTIVE") bind(projectinstance.ErrStageNotCompleted, "INSTANCE_STAGE_NOT_COMPLETED") bind(projectinstance.ErrRecheckAckRequired, "INSTANCE_RECHECK_REQUIRED") bind(projectinstance.ErrInvalidStage, "INSTANCE_INVALID_STAGE") bind(projectinstance.ErrInstanceRevisionConflict, "INSTANCE_REVISION_CONFLICT") bind(projectinstance.ErrTerminationNotAllowed, "INSTANCE_TERMINATION_DENIED") bind(projectinstance.ErrAuthorOnlyPolicyDisabled, "INSTANCE_AUTHOR_ONLY_DISABLED") bind(projectinstance.ErrAuthorOnlyPolicyUnavailable, "INSTANCE_AUTHOR_ONLY_UNAVAILABLE") bind(projectinstance.ErrAuthorOnlyPolicyChanged, "INSTANCE_AUTHOR_ONLY_CHANGED") bind(projectinstance.ErrForbiddenDutyReassign, "INSTANCE_DUTY_FORBIDDEN") bind(projectinstance.ErrDocTaskNotFound, "INSTANCE_DOC_TASK_NOT_FOUND") bind(projectinstance.ErrLegacySwitchBlocked, "INSTANCE_LEGACY_SWITCH_BLOCKED") bind(projectinstance.ErrTaskScopeNotResolved, "TASK_SCOPE_UNRESOLVED") }