Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
8d7a4975
Unverified
Commit
8d7a4975
authored
Dec 26, 2025
by
NepetaLemon
Committed by
GitHub
Dec 26, 2025
Browse files
refactor: 自定义业务错误 (#33)
* refactor: 自定义业务错误 * refactor: 隐藏服务器错误与统一 panic 响应
parent
b31698b9
Changes
8
Hide whitespace changes
Inline
Side-by-side
backend/Makefile
View file @
8d7a4975
...
...
@@ -16,14 +16,14 @@ build-embed:
@
echo
"构建完成: bin/server (with embedded frontend)"
test-unit
:
@
go
test
./...
$(TEST_ARGS)
@
go
test
-tags
unit ./...
-count
=
1
test-integration
:
@
go
test
-tags
integration ./
internal/repository
-count
=
1
-race
-parallel
=
8
@
go
test
-tags
integration ./
...
-count
=
1
-race
-parallel
=
8
test-cover-integration
:
@
echo
"运行集成测试并生成覆盖率报告..."
@
go
test
-tags
=
integration
-cover
-coverprofile
=
coverage.out
-count
=
1
-race
-parallel
=
8 ./
internal/repository/
...
@
go
test
-tags
=
integration
-cover
-coverprofile
=
coverage.out
-count
=
1
-race
-parallel
=
8 ./...
@
go tool cover
-func
=
coverage.out |
tail
-1
@
go tool cover
-html
=
coverage.out
-o
coverage.html
@
echo
"覆盖率报告已生成: coverage.html"
...
...
backend/cmd/server/main.go
View file @
8d7a4975
...
...
@@ -84,7 +84,7 @@ func main() {
func
runSetupServer
()
{
r
:=
gin
.
New
()
r
.
Use
(
gin
.
Recovery
())
r
.
Use
(
middleware
.
Recovery
())
r
.
Use
(
middleware
.
CORS
())
// Register setup routes
...
...
backend/internal/infrastructure/errors/errors.go
View file @
8d7a4975
...
...
@@ -7,8 +7,9 @@ import (
)
const
(
UnknownCode
=
http
.
StatusInternalServerError
UnknownReason
=
""
UnknownCode
=
http
.
StatusInternalServerError
UnknownReason
=
""
UnknownMessage
=
"internal error"
)
type
Status
struct
{
...
...
@@ -153,5 +154,5 @@ func FromError(err error) *ApplicationError {
}
// Fall back to a generic internal error.
return
New
(
UnknownCode
,
UnknownReason
,
err
.
Error
()
)
.
WithCause
(
err
)
return
New
(
UnknownCode
,
UnknownReason
,
UnknownMessage
)
.
WithCause
(
err
)
}
backend/internal/infrastructure/errors/errors_test.go
View file @
8d7a4975
...
...
@@ -111,14 +111,14 @@ func TestFromError_Generic(t *testing.T) {
err
:
stderrors
.
New
(
"boom"
),
wantCode
:
UnknownCode
,
wantReason
:
UnknownReason
,
wantMsg
:
"boom"
,
wantMsg
:
UnknownMessage
,
},
{
name
:
"wrapped_plain_error"
,
err
:
fmt
.
Errorf
(
"wrap: %w"
,
io
.
EOF
),
wantCode
:
UnknownCode
,
wantReason
:
UnknownReason
,
wantMsg
:
"wrap: EOF"
,
wantMsg
:
UnknownMessage
,
},
}
...
...
backend/internal/middleware/recovery.go
0 → 100644
View file @
8d7a4975
package
middleware
import
(
"errors"
"net"
"net/http"
"os"
"strings"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/infrastructure/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/gin-gonic/gin"
)
// Recovery converts panics into the project's standard JSON error envelope.
//
// It preserves Gin's broken-pipe handling by not attempting to write a response
// when the client connection is already gone.
func
Recovery
()
gin
.
HandlerFunc
{
return
gin
.
CustomRecoveryWithWriter
(
gin
.
DefaultErrorWriter
,
func
(
c
*
gin
.
Context
,
recovered
any
)
{
recoveredErr
,
_
:=
recovered
.
(
error
)
if
isBrokenPipe
(
recoveredErr
)
{
if
recoveredErr
!=
nil
{
_
=
c
.
Error
(
recoveredErr
)
}
c
.
Abort
()
return
}
if
c
.
Writer
.
Written
()
{
c
.
Abort
()
return
}
response
.
ErrorWithDetails
(
c
,
http
.
StatusInternalServerError
,
infraerrors
.
UnknownMessage
,
infraerrors
.
UnknownReason
,
nil
,
)
c
.
Abort
()
})
}
func
isBrokenPipe
(
err
error
)
bool
{
if
err
==
nil
{
return
false
}
var
opErr
*
net
.
OpError
if
!
errors
.
As
(
err
,
&
opErr
)
{
return
false
}
var
syscallErr
*
os
.
SyscallError
if
!
errors
.
As
(
opErr
.
Err
,
&
syscallErr
)
{
return
false
}
msg
:=
strings
.
ToLower
(
syscallErr
.
Error
())
return
strings
.
Contains
(
msg
,
"broken pipe"
)
||
strings
.
Contains
(
msg
,
"connection reset by peer"
)
}
backend/internal/middleware/recovery_test.go
0 → 100644
View file @
8d7a4975
//go:build unit
package
middleware
import
(
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/infrastructure/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func
TestRecovery
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
tests
:=
[]
struct
{
name
string
handler
gin
.
HandlerFunc
wantHTTPCode
int
wantBody
response
.
Response
}{
{
name
:
"panic_returns_standard_json_500"
,
handler
:
func
(
c
*
gin
.
Context
)
{
panic
(
"boom"
)
},
wantHTTPCode
:
http
.
StatusInternalServerError
,
wantBody
:
response
.
Response
{
Code
:
http
.
StatusInternalServerError
,
Message
:
infraerrors
.
UnknownMessage
,
},
},
{
name
:
"no_panic_passthrough"
,
handler
:
func
(
c
*
gin
.
Context
)
{
response
.
Success
(
c
,
gin
.
H
{
"ok"
:
true
})
},
wantHTTPCode
:
http
.
StatusOK
,
wantBody
:
response
.
Response
{
Code
:
0
,
Message
:
"success"
,
Data
:
map
[
string
]
any
{
"ok"
:
true
},
},
},
{
name
:
"panic_after_write_does_not_override_body"
,
handler
:
func
(
c
*
gin
.
Context
)
{
response
.
Success
(
c
,
gin
.
H
{
"ok"
:
true
})
panic
(
"boom"
)
},
wantHTTPCode
:
http
.
StatusOK
,
wantBody
:
response
.
Response
{
Code
:
0
,
Message
:
"success"
,
Data
:
map
[
string
]
any
{
"ok"
:
true
},
},
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
r
:=
gin
.
New
()
r
.
Use
(
Recovery
())
r
.
GET
(
"/t"
,
tt
.
handler
)
w
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/t"
,
nil
)
r
.
ServeHTTP
(
w
,
req
)
require
.
Equal
(
t
,
tt
.
wantHTTPCode
,
w
.
Code
)
var
got
response
.
Response
require
.
NoError
(
t
,
json
.
Unmarshal
(
w
.
Body
.
Bytes
(),
&
got
))
require
.
Equal
(
t
,
tt
.
wantBody
,
got
)
})
}
}
backend/internal/pkg/response/response_test.go
View file @
8d7a4975
...
...
@@ -143,7 +143,7 @@ func TestErrorFrom(t *testing.T) {
wantHTTPCode
:
http
.
StatusInternalServerError
,
wantBody
:
Response
{
Code
:
http
.
StatusInternalServerError
,
Message
:
"boom"
,
Message
:
infraerrors
.
UnknownMessage
,
},
},
}
...
...
backend/internal/server/http.go
View file @
8d7a4975
...
...
@@ -3,6 +3,7 @@ package server
import
(
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/handler"
"github.com/Wei-Shaw/sub2api/internal/middleware"
"github.com/Wei-Shaw/sub2api/internal/repository"
"github.com/Wei-Shaw/sub2api/internal/service"
"net/http"
...
...
@@ -25,7 +26,7 @@ func ProvideRouter(cfg *config.Config, handlers *handler.Handlers, services *ser
}
r
:=
gin
.
New
()
r
.
Use
(
gin
.
Recovery
())
r
.
Use
(
middleware
.
Recovery
())
return
SetupRouter
(
r
,
cfg
,
handlers
,
services
,
repos
)
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment