fix version 1

This commit is contained in:
2026-09-22 21:15:34 +08:00
parent 9f86366638
commit d63d7e8b3a
277 changed files with 9959 additions and 1514 deletions

View File

@@ -35,12 +35,24 @@ func New(key string, expireSeconds int64, anonymous []string) (*Server, error) {
engine.Use(gin.Logger(), gin.Recovery())
return &Server{
GRPC: grpcServer,
Gateway: gwRuntime.NewServeMux(),
Gateway: gwRuntime.NewServeMux(gwRuntime.WithRoutingErrorHandler(routeMissHandler)),
HTTP: engine,
auth: auth,
}, nil
}
// routeMissHandler 是 grpc-gateway 的路由错误处理器:只有"路由未命中"才会被标记,
// 随后仍按默认行为写出错误体。业务 handler 返回的 NotFound 走的是 errorHandler
// 不会被标记,因而其响应能原样透出而不会被 Gin 的纯文本 404 覆盖。
func routeMissHandler(ctx context.Context, mux *gwRuntime.ServeMux, marshaler gwRuntime.Marshaler, w http.ResponseWriter, r *http.Request, httpStatus int) {
if httpStatus == http.StatusNotFound {
if recorder, ok := w.(routingMissRecorder); ok {
recorder.markRoutingMiss()
}
}
gwRuntime.DefaultRoutingErrorHandler(ctx, mux, marshaler, w, r, httpStatus)
}
func (s *Server) Start(grpcAddr, httpAddr string) error {
grpcListener, err := net.Listen("tcp", grpcAddr)
if err != nil {
@@ -60,18 +72,9 @@ func (s *Server) Start(grpcAddr, httpAddr string) error {
}
s.HTTP.POST("/rpc/:module/:service/:method", s.dynamic.handle)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
recorder := newBufferedResponse()
s.Gateway.ServeHTTP(recorder, r)
if recorder.status != http.StatusNotFound {
recorder.flush(w)
return
}
s.HTTP.ServeHTTP(w, r)
})
s.http = &http.Server{
Addr: httpAddr,
Handler: s.auth.httpMiddleware(handler),
Handler: s.auth.httpMiddleware(s.handler()),
ReadHeaderTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20,
@@ -89,6 +92,21 @@ func (s *Server) Start(grpcAddr, httpAddr string) error {
return serveErr
}
// handler 先让 grpc-gateway 处理请求,仅当网关确实未命中路由时才回退到 Gin
// (动态 RPC 与原生的 /rest 路由都注册在 Gin 上)。业务返回的 404/NotFound
// 属于网关的正常响应,必须原样透出。
func (s *Server) handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
recorder := newBufferedResponse()
s.Gateway.ServeHTTP(recorder, r)
if !recorder.routingMiss {
recorder.flush(w)
return
}
s.HTTP.ServeHTTP(w, r)
})
}
func (s *Server) Stop(ctx context.Context) error {
stopped := make(chan struct{})
go func() {
@@ -109,10 +127,17 @@ func (s *Server) Stop(ctx context.Context) error {
return s.http.Shutdown(ctx)
}
// routingMissRecorder 由 bufferedResponse 实现,用于区分"网关未命中路由"与业务 NotFound。
type routingMissRecorder interface {
markRoutingMiss()
}
type bufferedResponse struct {
header http.Header
body bytes.Buffer
status int
// routingMiss 表示该 404 来自 grpc-gateway 的路由错误处理,而非业务响应。
routingMiss bool
}
func newBufferedResponse() *bufferedResponse {
@@ -122,6 +147,7 @@ func newBufferedResponse() *bufferedResponse {
func (r *bufferedResponse) Header() http.Header { return r.header }
func (r *bufferedResponse) WriteHeader(status int) { r.status = status }
func (r *bufferedResponse) Write(data []byte) (int, error) { return r.body.Write(data) }
func (r *bufferedResponse) markRoutingMiss() { r.routingMiss = true }
func (r *bufferedResponse) flush(w http.ResponseWriter) {
for key, values := range r.header {
for _, value := range values {

View File

@@ -3,6 +3,7 @@ package server
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
@@ -22,3 +23,38 @@ func TestHTTPRouterIsAvailable(t *testing.T) {
t.Fatalf("unexpected status: %d", response.Code)
}
}
// TestGatewayDispatchKeepsBusinessNotFound 守住 404 分流:网关未命中路由才回退 Gin
// 业务 handler 返回的 404 必须原样透出,不能被 Gin 的纯文本 404 覆盖。
func TestGatewayDispatchKeepsBusinessNotFound(t *testing.T) {
srv, err := New("0123456789abcdef0123456789abcdef", 3600, nil)
if err != nil {
t.Fatal(err)
}
if err := srv.Gateway.HandlePath(http.MethodPost, "/demo.Thing/Get", func(w http.ResponseWriter, _ *http.Request, _ map[string]string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"code":5,"message":"record not found"}`))
}); err != nil {
t.Fatal(err)
}
srv.HTTP.POST("/rest/demo/ping", func(c *gin.Context) { c.String(http.StatusOK, "gin") })
handler := srv.handler()
t.Run("business not found", func(t *testing.T) {
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/demo.Thing/Get", nil))
if response.Code != http.StatusNotFound || !strings.Contains(response.Body.String(), "record not found") {
t.Fatalf("business 404 was overwritten: status=%d body=%s", response.Code, response.Body.String())
}
})
t.Run("gateway route miss", func(t *testing.T) {
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/rest/demo/ping", nil))
if response.Code != http.StatusOK || response.Body.String() != "gin" {
t.Fatalf("gin fallback failed: status=%d body=%s", response.Code, response.Body.String())
}
})
}