208 lines
9.0 KiB
Go
208 lines
9.0 KiB
Go
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")
|
|
}
|