- upload.go: multipart上传(owner/admin+审计)→ {url,size,mime,name}; MaxBytesReader 5MB + DetectContentType嗅探 + 白名单raster(拒SVG/HTML) + crypto/rand 128-bit随机名 + 0700目录
- 静态服务: 同端口 GET /v1/files/{name}, 能力URL, 路径穿越全形态拒绝(.. / %2f / %2e%2e), nosniff, Range
- errors: ErrPayloadTooLarge(413)/ErrUnsupportedMediaType(415)
- server.go 路由+Deps.DataDir; wire.go resolveDataDir 注入
- 8测试+真实二进制E2E, build/vet/test绿+交叉编译
- Docs/api-client v0.24 + api-reference §0.3 同步
112 lines
4.7 KiB
Go
112 lines
4.7 KiB
Go
// Package errors provides the unified application error type used throughout
|
|
// Cyrene v2.
|
|
//
|
|
// Design (DevDocs/02-development-constraints.md §四):
|
|
//
|
|
// - An AppError carries a stable machine-readable Code, a user-safe Message,
|
|
// an optional Cause, and an HTTP status code.
|
|
// - Errors must be wrapped with %w semantics so errors.Is/errors.As can
|
|
// traverse the chain, and the original Cause is preserved for callers that
|
|
// need the raw error (logging, diagnostics, errors.Is).
|
|
// - Internal errors are never exposed directly to users: Message is the only
|
|
// user-facing text, while Cause keeps the underlying error out of the
|
|
// client response (see ToJSON).
|
|
// - Each module defines its own ErrXxx sentinels; the common ones live here.
|
|
package errors
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
// AppError is the unified error type for Cyrene v2.
|
|
//
|
|
// It satisfies the error interface (Error returns Message) and the unwrap
|
|
// contract (Unwrap returns Cause), so errors.Is/errors.As descend through the
|
|
// chain. The zero value is a usable AppError with an empty Code and Message.
|
|
type AppError struct {
|
|
// Code is a stable, machine-readable error code (e.g. "not_found").
|
|
Code string
|
|
|
|
// Message is the user-safe, readable text. Internal detail never goes here;
|
|
// keep the raw cause in Cause instead.
|
|
Message string
|
|
|
|
// Cause is the underlying error that produced this AppError. It is preserved
|
|
// for errors.Is/errors.As traversal and for logging. May be nil.
|
|
Cause error
|
|
|
|
// HTTPCode is the HTTP status to return with this error. 0 means "unset",
|
|
// which HTTPStatus resolves to http.StatusInternalServerError (500).
|
|
HTTPCode int
|
|
}
|
|
|
|
// Error returns the user-facing Message. It implements the error interface.
|
|
func (e *AppError) Error() string { return e.Message }
|
|
|
|
// Unwrap returns the Cause so errors.Is/errors.As can traverse the chain.
|
|
// It returns nil when there is no underlying cause.
|
|
func (e *AppError) Unwrap() error { return e.Cause }
|
|
|
|
// New creates an AppError with the given Code and Message and no cause.
|
|
// HTTPCode is left 0 (HTTPStatus maps it to 500 unless overridden).
|
|
func New(code, msg string) *AppError {
|
|
return &AppError{Code: code, Message: msg}
|
|
}
|
|
|
|
// Wrap creates an AppError that wraps err as its Cause, with a formatted
|
|
// Message and the given Code. The Message is produced with fmt.Sprintf(format,
|
|
// args...) and is intended to be user-safe; the original error is preserved in
|
|
// Cause so errors.Is(err, original) and errors.As still work.
|
|
//
|
|
// The wrapped error inherits its HTTP status from err (see HTTPStatus), so
|
|
// wrapping a sentinel such as ErrNotFound keeps status 404.
|
|
func Wrap(err error, code, format string, args ...any) *AppError {
|
|
return &AppError{
|
|
Code: code,
|
|
Message: fmt.Sprintf(format, args...),
|
|
Cause: err,
|
|
HTTPCode: HTTPStatus(err),
|
|
}
|
|
}
|
|
|
|
// newWithHTTP builds a sentinel AppError that carries an explicit HTTP status.
|
|
func newWithHTTP(code, msg string, httpCode int) *AppError {
|
|
return &AppError{Code: code, Message: msg, HTTPCode: httpCode}
|
|
}
|
|
|
|
// Common application error sentinels, mirroring the HTTP status codes they map
|
|
// to. Modules should define their own ErrXxx and may wrap these as Causes.
|
|
var (
|
|
// ErrInternal is the generic internal error (500).
|
|
ErrInternal = newWithHTTP("internal", "internal error", http.StatusInternalServerError)
|
|
|
|
// ErrNotFound means a requested resource does not exist (404).
|
|
ErrNotFound = newWithHTTP("not_found", "not found", http.StatusNotFound)
|
|
|
|
// ErrConflict means the request conflicts with the current state (409).
|
|
ErrConflict = newWithHTTP("conflict", "conflict", http.StatusConflict)
|
|
|
|
// ErrUnauthorized means the caller is not authenticated (401).
|
|
ErrUnauthorized = newWithHTTP("unauthorized", "unauthorized", http.StatusUnauthorized)
|
|
|
|
// ErrForbidden means the caller is authenticated but not allowed (403).
|
|
ErrForbidden = newWithHTTP("forbidden", "forbidden", http.StatusForbidden)
|
|
|
|
// ErrBadRequest means the request is malformed (400).
|
|
ErrBadRequest = newWithHTTP("bad_request", "bad request", http.StatusBadRequest)
|
|
|
|
// ErrPayloadTooLarge means the request body exceeds the allowed size (413).
|
|
ErrPayloadTooLarge = newWithHTTP("payload_too_large", "payload too large", http.StatusRequestEntityTooLarge)
|
|
|
|
// ErrUnsupportedMediaType means the request carries a media type the endpoint
|
|
// does not accept (415).
|
|
ErrUnsupportedMediaType = newWithHTTP("unsupported_media_type", "unsupported media type", http.StatusUnsupportedMediaType)
|
|
|
|
// ErrRateLimited means the caller exceeded a rate limit (429).
|
|
ErrRateLimited = newWithHTTP("rate_limited", "rate limited", http.StatusTooManyRequests)
|
|
|
|
// ErrTimeout means the operation exceeded its deadline (503).
|
|
ErrTimeout = newWithHTTP("timeout", "timeout", http.StatusServiceUnavailable)
|
|
)
|