Refactor error handling

This commit is contained in:
2025-06-12 11:33:13 +10:00
parent 2a042968d0
commit b80afd97f1
16 changed files with 304 additions and 122 deletions

View File

@ -2,9 +2,9 @@ package database
import (
"encoding/json"
"errors"
"path/filepath"
"github.com/bestnite/sub2clash/common"
"github.com/bestnite/sub2clash/model"
"go.etcd.io/bbolt"
@ -17,13 +17,16 @@ func ConnectDB() error {
db, err := bbolt.Open(path, 0600, nil)
if err != nil {
return err
return common.NewDatabaseConnectError(err)
}
DB = db
return db.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("ShortLinks"))
return err
if err != nil {
return common.NewDatabaseConnectError(err)
}
return nil
})
}
@ -33,7 +36,7 @@ func FindShortLinkByHash(hash string) (*model.ShortLink, error) {
b := tx.Bucket([]byte("ShortLinks"))
v := b.Get([]byte(hash))
if v == nil {
return errors.New("ShortLink not found")
return common.NewRecordNotFoundError("ShortLink", hash)
}
return json.Unmarshal(v, &shortLink)
})

192
common/errors.go Normal file
View File

@ -0,0 +1,192 @@
package common
import (
"errors"
"fmt"
)
// CommonError represents a structured error type for the common package
type CommonError struct {
Code ErrorCode
Message string
Cause error
}
// ErrorCode represents different types of errors
type ErrorCode string
const (
// Directory operation errors
ErrDirCreation ErrorCode = "DIRECTORY_CREATION_FAILED"
ErrDirAccess ErrorCode = "DIRECTORY_ACCESS_FAILED"
// File operation errors
ErrFileNotFound ErrorCode = "FILE_NOT_FOUND"
ErrFileRead ErrorCode = "FILE_READ_FAILED"
ErrFileWrite ErrorCode = "FILE_WRITE_FAILED"
ErrFileCreate ErrorCode = "FILE_CREATE_FAILED"
// Network operation errors
ErrNetworkRequest ErrorCode = "NETWORK_REQUEST_FAILED"
ErrNetworkResponse ErrorCode = "NETWORK_RESPONSE_FAILED"
// Template and configuration errors
ErrTemplateLoad ErrorCode = "TEMPLATE_LOAD_FAILED"
ErrTemplateParse ErrorCode = "TEMPLATE_PARSE_FAILED"
ErrConfigInvalid ErrorCode = "CONFIG_INVALID"
// Subscription errors
ErrSubscriptionLoad ErrorCode = "SUBSCRIPTION_LOAD_FAILED"
ErrSubscriptionParse ErrorCode = "SUBSCRIPTION_PARSE_FAILED"
// Regex errors
ErrRegexCompile ErrorCode = "REGEX_COMPILE_FAILED"
ErrRegexInvalid ErrorCode = "REGEX_INVALID"
// Database errors
ErrDatabaseConnect ErrorCode = "DATABASE_CONNECTION_FAILED"
ErrDatabaseQuery ErrorCode = "DATABASE_QUERY_FAILED"
ErrRecordNotFound ErrorCode = "RECORD_NOT_FOUND"
// Validation errors
ErrValidation ErrorCode = "VALIDATION_FAILED"
ErrInvalidInput ErrorCode = "INVALID_INPUT"
)
// Error returns the string representation of the error
func (e *CommonError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// Unwrap returns the underlying error
func (e *CommonError) Unwrap() error {
return e.Cause
}
// Is allows error comparison
func (e *CommonError) Is(target error) bool {
if t, ok := target.(*CommonError); ok {
return e.Code == t.Code
}
return false
}
// NewError creates a new CommonError
func NewError(code ErrorCode, message string, cause error) *CommonError {
return &CommonError{
Code: code,
Message: message,
Cause: cause,
}
}
// NewSimpleError creates a new CommonError without a cause
func NewSimpleError(code ErrorCode, message string) *CommonError {
return &CommonError{
Code: code,
Message: message,
}
}
// Convenience constructors for common error types
// Directory errors
func NewDirCreationError(dirPath string, cause error) *CommonError {
return NewError(ErrDirCreation, fmt.Sprintf("failed to create directory: %s", dirPath), cause)
}
func NewDirAccessError(dirPath string, cause error) *CommonError {
return NewError(ErrDirAccess, fmt.Sprintf("failed to access directory: %s", dirPath), cause)
}
// File errors
func NewFileNotFoundError(filePath string) *CommonError {
return NewSimpleError(ErrFileNotFound, fmt.Sprintf("file not found: %s", filePath))
}
func NewFileReadError(filePath string, cause error) *CommonError {
return NewError(ErrFileRead, fmt.Sprintf("failed to read file: %s", filePath), cause)
}
func NewFileWriteError(filePath string, cause error) *CommonError {
return NewError(ErrFileWrite, fmt.Sprintf("failed to write file: %s", filePath), cause)
}
func NewFileCreateError(filePath string, cause error) *CommonError {
return NewError(ErrFileCreate, fmt.Sprintf("failed to create file: %s", filePath), cause)
}
// Network errors
func NewNetworkRequestError(url string, cause error) *CommonError {
return NewError(ErrNetworkRequest, fmt.Sprintf("network request failed for URL: %s", url), cause)
}
func NewNetworkResponseError(message string, cause error) *CommonError {
return NewError(ErrNetworkResponse, message, cause)
}
// Template errors
func NewTemplateLoadError(template string, cause error) *CommonError {
return NewError(ErrTemplateLoad, fmt.Sprintf("failed to load template: %s", template), cause)
}
func NewTemplateParseError(cause error) *CommonError {
return NewError(ErrTemplateParse, "failed to parse template", cause)
}
// Subscription errors
func NewSubscriptionLoadError(url string, cause error) *CommonError {
return NewError(ErrSubscriptionLoad, fmt.Sprintf("failed to load subscription: %s", url), cause)
}
func NewSubscriptionParseError(cause error) *CommonError {
return NewError(ErrSubscriptionParse, "failed to parse subscription", cause)
}
// Regex errors
func NewRegexCompileError(pattern string, cause error) *CommonError {
return NewError(ErrRegexCompile, fmt.Sprintf("failed to compile regex pattern: %s", pattern), cause)
}
func NewRegexInvalidError(paramName string, cause error) *CommonError {
return NewError(ErrRegexInvalid, fmt.Sprintf("invalid regex in parameter: %s", paramName), cause)
}
// Database errors
func NewDatabaseConnectError(cause error) *CommonError {
return NewError(ErrDatabaseConnect, "failed to connect to database", cause)
}
func NewRecordNotFoundError(recordType string, id string) *CommonError {
return NewSimpleError(ErrRecordNotFound, fmt.Sprintf("%s not found: %s", recordType, id))
}
// Validation errors
func NewValidationError(field string, message string) *CommonError {
return NewSimpleError(ErrValidation, fmt.Sprintf("validation failed for %s: %s", field, message))
}
func NewInvalidInputError(paramName string, value string) *CommonError {
return NewSimpleError(ErrInvalidInput, fmt.Sprintf("invalid input for parameter %s: %s", paramName, value))
}
// IsErrorCode checks if an error has a specific error code
func IsErrorCode(err error, code ErrorCode) bool {
var commonErr *CommonError
if errors.As(err, &commonErr) {
return commonErr.Code == code
}
return false
}
// GetErrorCode extracts the error code from an error
func GetErrorCode(err error) (ErrorCode, bool) {
var commonErr *CommonError
if errors.As(err, &commonErr) {
return commonErr.Code, true
}
return "", false
}

View File

@ -1,7 +1,6 @@
package common
import (
"errors"
"os"
)
@ -18,13 +17,13 @@ func MKDir(dir string) error {
func MkEssentialDir() error {
if err := MKDir("subs"); err != nil {
return errors.New("create subs dir failed" + err.Error())
return NewDirCreationError("subs", err)
}
if err := MKDir("logs"); err != nil {
return errors.New("create logs dir failed" + err.Error())
return NewDirCreationError("logs", err)
}
if err := MKDir("data"); err != nil {
return errors.New("create data dir failed" + err.Error())
return NewDirCreationError("data", err)
}
return nil
}

View File

@ -3,7 +3,6 @@ package common
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/url"
@ -110,26 +109,26 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error())
return nil, NewTemplateLoadError(template, err)
}
} else {
unescape, err := url.QueryUnescape(template)
if err != nil {
return nil, errors.New("加载模板失败: " + err.Error())
return nil, NewTemplateLoadError(template, err)
}
templateBytes, err = LoadTemplate(unescape)
if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error())
return nil, NewTemplateLoadError(unescape, err)
}
}
err = yaml.Unmarshal(templateBytes, &temp)
if err != nil {
logger.Logger.Debug("parse template failed", zap.Error(err))
return nil, errors.New("解析模板失败: " + err.Error())
return nil, NewTemplateParseError(err)
}
var proxyList []P.Proxy
@ -143,7 +142,7 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
logger.Logger.Debug(
"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
)
return nil, errors.New("加载订阅失败: " + err.Error())
return nil, NewSubscriptionLoadError(query.Subs[i], err)
}
err = yaml.Unmarshal(data, &sub)
@ -161,7 +160,7 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
zap.String("data", string(data)),
zap.Error(err),
)
return nil, errors.New("加载订阅失败: " + err.Error())
return nil, NewSubscriptionParseError(err)
}
p := parser.ParseProxies(strings.Split(base64, "\n")...)
newProxies = p
@ -193,7 +192,7 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
yamlBytes, err := yaml.Marshal(proxyList[i])
if err != nil {
logger.Logger.Debug("marshal proxy failed", zap.Error(err))
return nil, errors.New("marshal proxy failed: " + err.Error())
return nil, fmt.Errorf("marshal proxy failed: %w", err)
}
key := string(yamlBytes)
if _, exist := proxies[key]; !exist {
@ -209,7 +208,7 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
removeReg, err := regexp.Compile(query.Remove)
if err != nil {
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
return nil, errors.New("remove 参数非法: " + err.Error())
return nil, NewRegexInvalidError("remove", err)
}
if removeReg.MatchString(proxyList[i].Name) {
@ -227,7 +226,7 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
replaceReg, err := regexp.Compile(v)
if err != nil {
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
return nil, errors.New("replace 参数非法: " + err.Error())
return nil, NewRegexInvalidError("replace", err)
}
replaceRegs = append(replaceRegs, replaceReg)
}
@ -312,15 +311,15 @@ func FetchSubscriptionUserInfo(url string, userAgent string, retryTimes int) (st
resp, err := client.R().SetHeader("User-Agent", userAgent).Head(url)
if err != nil {
logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err))
return "", err
return "", NewNetworkRequestError(url, err)
}
defer resp.Body.Close()
if userInfo := resp.Header().Get("subscription-userinfo"); userInfo != "" {
return userInfo, nil
}
logger.Logger.Debug("目标 URL 未返回 subscription-userinfo 头", zap.Error(err))
return "", err
logger.Logger.Debug("subscription-userinfo header not found in response")
return "", NewNetworkResponseError("subscription-userinfo header not found", nil)
}
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {

View File

@ -1,7 +1,6 @@
package common
import (
"errors"
"io"
"os"
)
@ -23,5 +22,5 @@ func LoadTemplate(templatePath string) ([]byte, error) {
}
return result, nil
}
return nil, errors.New("模板文件不存在")
return nil, NewFileNotFoundError(templatePath)
}