fix version 1
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user