Files
weichen f3f23a3018 feat(errcode): 用稳定错误码记录 HTTP 与异步失败
失败响应此前不写日志,request_id 无法对照原因。注册表不覆盖处理器已经给出的 HTTP 状态。
2026-09-28 02:27:16 +00:00

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")
}