Repository: alibaba/sentinel-golang
Pull request #565 (2024-06-26 07:11:11)

Feat governance gray v3
- add fallback rule
- support fallback for gin and go-zero web
- support mock for system rule

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/alibaba/sentinel-golang?pullRequest=565) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/alibaba/sentinel-golang?pullRequest=565) before we can accept your contribution.<br/><hr/>**如漫** seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please [add the email address used for this commit to your account](https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/#commits-are-not-linked-to-any-user).<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/alibaba/sentinel-golang?pullRequest=565) it.</sub>

Patch:
diff --git a/core/fallback/rule.go b/core/fallback/rule.go
new file mode 100644
index 0000000..012d91b
--- /dev/null
+++ b/core/fallback/rule.go
@@ -0,0 +1,53 @@
+// Copyright 1999-2020 Alibaba Group Holding Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fallback
+
+type TargetResourceType int64
+
+const (
+	WebResourceType TargetResourceType = 1
+	RpcResourceType TargetResourceType = 2
+)
+
+type FunctionType int64
+
+const (
+	FlowType    FunctionType = 1
+	Isolation   FunctionType = 6
+	HotspotRpc  FunctionType = 4
+	HotspotHttp FunctionType = 11
+)
+
+type Rule struct {
+	TargetResourceType TargetResourceType        `json:"targetResourceType"`
+	TargetMap          map[string][]FunctionType `json:"targetMap"`
+	FallbackBehavior   interface{}               `json:"fallbackBehavior"`
+}
+
+type WebBlockFallbackBehavior struct {
+	WebFallbackMode    int64  `json:"webFallbackMode"` // 0: return, 1: redirect
+	WebRespStatusCode  int64  `json:"webRespStatusCode"`
+	WebRespMessage     string `json:"webRespMessage"`
+	WebRespContentType int64  `json:"webRespContentType"` // 0: test, 1: json
+	WebRedirectUrl     string `json:"webRedirectUrl"`
+}
+
+type RpcBlockFallbackBehavior struct {
+	RpcFallbackMode             int64  `json:"rpcFallbackMode"`
+	RpcFallbackCacheMode        int64  `json:"rpcFallbackCacheMode"`
+	RpcRespFallbackClassName    string `json:"rpcRespFallbackClassName"`
+	RpcFallbackExceptionMessage string `json:"rpcFallbackExceptionMessage"`
+	RpcRespContentBody          string `json:"rpcRespContentBody"`
+}
diff --git a/core/fallback/rule_manager.go b/core/fallback/rule_manager.go
new file mode 100644
index 0000000..c10594e
--- /dev/null
+++ b/core/fallback/rule_manager.go
@@ -0,0 +1,174 @@
+// Copyright 1999-2020 Alibaba Group Holding Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fallback
+
+import (
+	"encoding/json"
+	"github.com/alibaba/sentinel-golang/logging"
+	"github.com/alibaba/sentinel-golang/util"
+	"reflect"
+	"sync"
+)
+
+var (
+	webRuleMap      = make(map[string]map[FunctionType]*WebBlockFallbackBehavior)
+	rpcRuleMap      = make(map[string]map[FunctionType]*RpcBlockFallbackBehavior)
+	currentWebRules = make(map[string]map[FunctionType]*WebBlockFallbackBehavior)
+	currentRpcRules = make(map[string]map[FunctionType]*RpcBlockFallbackBehavior)
+	webRwMux        = &sync.RWMutex{}
+	rpcRwMux        = &sync.RWMutex{}
+	updateRuleMux   = &sync.RWMutex{}
+)
+
+func isValidWebFallbackBehavior(behavior *WebBlockFallbackBehavior) bool {
+	if behavior == nil {
+		return false
+	}
+	if behavior.WebRespContentType != 0 && behavior.WebRespContentType != 1 {
+		return false
+	}
+	if behavior.WebRespStatusCode < 0 || behavior.WebRespStatusCode > 600 {
+		return false
+	}
+	return true
+}
+
+func isValidRpcFallbackBehavior(behavior *RpcBlockFallbackBehavior) bool {
+	if behavior == nil {
+		return false
+	}
+	if behavior.RpcFallbackMode != 0 && behavior.RpcFallbackMode != 1 {
+		return false
+	}
+	return true
+}
+
+func LoadRules(rules []*Rule) (bool, error) {
+	resWebRuleMap := make(map[string]map[FunctionType]*WebBlockFallbackBehavior)
+	resRpcRuleMap := make(map[string]map[FunctionType]*RpcBlockFallbackBehavior)
+	for _, rule := range rules {
+		b, err := json.Marshal(rule.FallbackBehavior)
+		if err != nil {
+			logging.Warn("[Fallback] marshal web fall back behavior failed", "reason", err.Error())
+			continue
+		}
+		switch rule.TargetResourceType {
+		case WebResourceType:
+			var webBehavior *WebBlockFallbackBehavior
+			err := json.Unmarshal(b, &webBehavior)
+			if err != nil {
+				logging.Warn("[Fallback] unmarshal web fall back behavior failed", "reason", err.Error())
+				continue
+			}
+			if !isValidWebFallbackBehavior(webBehavior) {
+				logging.Warn("[Fallback] invalid web fall back behavior", "behavior", webBehavior)
+				continue
+			}
+
+			for resource, funcTypeList := range rule.TargetMap {
+				if resource == "" || len(funcTypeList) == 0 {
+					continue
+				}
+				var behaviorMap map[FunctionType]*WebBlockFallbackBehavior
+				var ok bool
+				if behaviorMap, ok = resWebRuleMap[resource]; !ok {
+					behaviorMap = make(map[FunctionType]*WebBlockFallbackBehavior)
+					resWebRuleMap[resource] = behaviorMap
+				}
+
+				for _, functionType := range funcTypeList {
+					behaviorMap[functionType] = webBehavior
+				}
+			}
+		case RpcResourceType:
+			var rpcBehavior *RpcBlockFallbackBehavior
+			err := json.Unmarshal(b, &rpcBehavior)
+			if err != nil {
+				logging.Warn("[Fallback] unmarshal rpc fall back behavior failed", "reason", err.Error())
+				continue
+			}
+			if !isValidRpcFallbackBehavior(rpcBehavior) {
+				logging.Warn("[Fallback] invalid rpc fall back behavior", "behavior", rpcBehavior)
+				continue
+			}
+
+			for resource, funcTypeList := range rule.TargetMap {
+				var behaviorMap map[FunctionType]*RpcBlockFallbackBehavior
+				var ok bool
+				if behaviorMap, ok = resRpcRuleMap[resource]; !ok {
+					behaviorMap = make(map[FunctionType]*RpcBlockFallbackBehavior)
+					resRpcRuleMap[resource] = behaviorMap
+				}
+
+				for _, functionType := range funcTypeList {
+					behaviorMap[functionType] = rpcBehavior
+				}
+			}
+		default:
+			logging.Warn("[Fallback] unsupported resource type", "resourceType", rule.TargetResourceType)
+			continue
+		}
+	}
+
+	updateRuleMux.Lock()
+	defer updateRuleMux.Unlock()
+	var err error
+	var updated bool
+	isEqual := reflect.DeepEqual(currentWebRules, resWebRuleMap)
+	if !isEqual {
+		updateErr := onWebRuleUpdate(resWebRuleMap)
+		if updateErr != nil {
+			logging.Error(updateErr, "[Fallback] update web rule failed")
+			err = updateErr
+		} else {
+			updated = true
+		}
+	} else {
+		logging.Info("[Fallback] Web load rules is the same with current rules, so ignore load operation.")
+	}
+	isEqual = reflect.DeepEqual(currentRpcRules, resRpcRuleMap)
+	if !isEqual {
+		updateErr := onRpcRuleUpdate(resRpcRuleMap)
+		if updateErr != nil {
+			logging.Error(updateErr, "[Fallback] update rpc rule failed")
+			err = updateErr
+		} else {
+			updated = true
+		}
+	} else {
+		logging.Info("[Fallback] Rpc load rules is the same with current rules, so ignore load operation.")
+	}
+	return updated, err
+}
+
+func onWebRuleUpdate(rawWebRuleMap map[string]map[FunctionType]*WebBlockFallbackBehavior) error {
+	start := util.CurrentTimeNano()
+	webRwMux.Lock()
+	webRuleMap = rawWebRuleMap
+	webRwMux.Unlock()
+	currentWebRules = rawWebRuleMap
+	logging.Debug("[Fallback onWebRuleUpdate] Time statistic(ns) for updating web fallback rule", "timeCost", util.CurrentTimeNano()-start)
+	return nil
+}
+
+func onRpcRuleUpdate(rawRpcRuleMap map[string]map[FunctionType]*RpcBlockFallbackBehavior) error {
+	start := util.CurrentTimeNano()
+	rpcRwMux.Lock()
+	rpcRuleMap = rawRpcRuleMap
+	rpcRwMux.Unlock()
+	currentRpcRules = rawRpcRuleMap
+	logging.Debug("[Fallback onRpcRuleUpdate] Time statistic(ns) for updating rpc fallback rule", "timeCost", util.CurrentTimeNano()-start)
+	return nil
+}
diff --git a/core/fallback/slot.go b/core/fallback/slot.go
new file mode 100644
index 0000000..985de22
--- /dev/null
+++ b/core/fallback/slot.go
@@ -0,0 +1,70 @@
+// Copyright 1999-2020 Alibaba Group Holding Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fallback
+
+import (
+	"github.com/alibaba/sentinel-golang/core/base"
+)
+
+var blockType2FuncTypeMap = map[base.BlockType]FunctionType{
+	base.BlockTypeFlow:             FlowType,
+	base.BlockTypeIsolation:        Isolation,
+	base.BlockTypeHotSpotParamFlow: HotspotRpc,
+}
+
+func getFuncTypeByBlockType(blockType base.BlockType) (FunctionType, bool) {
+	funcType, ok := blockType2FuncTypeMap[blockType]
+	return funcType, ok
+}
+
+func GetWebFallbackBehavior(resource string, blockType base.BlockType) (*WebBlockFallbackBehavior, bool) {
+	functionType, ok := getFuncTypeByBlockType(blockType)
+	if !ok {
+		return nil, false
+	}
+
+	webRwMux.RLock()
+	defer webRwMux.RUnlock()
+
+	funcMap, ok := webRuleMap[resource]
+	if !ok {
+		return nil, false
+	}
+	behavior, ok := funcMap[functionType]
+	if !ok {
+		return nil, false
+	}
+	return behavior, true
+}
+
+func GetRpcFallbackBehavior(resource string, blockType base.BlockType) (*RpcBlockFallbackBehavior, bool) {
+	functionType, ok := getFuncTypeByBlockType(blockType)
+	if !ok {
+		return nil, false
+	}
+
+	rpcRwMux.RLock()
+	defer rpcRwMux.RUnlock()
+
+	funcMap, ok := rpcRuleMap[resource]
+	if !ok {
+		return nil, false
+	}
+	behavior, ok := funcMap[functionType]
+	if !ok {
+		return nil, false
+	}
+	return behavior, true
+}
diff --git a/core/system/rule.go b/core/system/rule.go
index 543fe20..eebbe2e 100644
--- a/core/system/rule.go
+++ b/core/system/rule.go
@@ -73,6 +73,24 @@ func (t AdaptiveStrategy) String() string {
 	}
 }
 
+type Action int32
+
+const (
+	Block Action = iota
+	Mock
+)
+
+func (a Action) String() string {
+	switch a {
+	case Block:
+		return "block"
+	case Mock:
+		return "mock"
+	default:
+		return fmt.Sprintf("unknown(%d)", a)
+	}
+}
+
 // Rule describes the policy for system resiliency.
 type Rule struct {
 	// ID represents the unique ID of the rule (optional).
@@ -84,6 +102,8 @@ type Rule struct {
 	TriggerCount float64 `json:"triggerCount"`
 	// Strategy represents the adaptive strategy.
 	Strategy AdaptiveStrategy `json:"strategy"`
+	// Action represents the action to take when the adaptive strategy is triggered.
+	Action Action `json:"action"`
 }
 
 func (r *Rule) String() string {
diff --git a/core/system/slot.go b/core/system/slot.go
index 5871ddc..16186e5 100644
--- a/core/system/slot.go
+++ b/core/system/slot.go
@@ -42,6 +42,9 @@ func (s *AdaptiveSlot) Check(ctx *base.EntryContext) *base.TokenResult {
 	rules := getRules()
 	result := ctx.RuleCheckResult
 	for _, rule := range rules {
+		if rule.Action == Mock { //TODO: return mock info for event
+			continue
+		}
 		passed, msg, snapshotValue := s.doCheckRule(rule)
 		if passed {
 			continue
@@ -93,7 +96,7 @@ func (s *AdaptiveSlot) doCheckRule(rule *Rule) (bool, string, float64) {
 		return true, "", l
 	case CpuUsage:
 		c := system_metric.CurrentCpuUsage()
-		if c > threshold {
+		if c > threshold*100 {
 			if rule.Strategy != BBR || !checkBbrSimple() {
 				msg = "system cpu usage check blocked"
 				return false, msg, c
diff --git a/pkg/adapters/gin/fallback.go b/pkg/adapters/gin/fallback.go
new file mode 100644
index 0000000..1319380
--- /dev/null
+++ b/pkg/adapters/gin/fallback.go
@@ -0,0 +1,37 @@
+package gin
+
+import (
+	"github.com/alibaba/sentinel-golang/core/base"
+	"github.com/alibaba/sentinel-golang/core/fallback"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func sentinelFallback(ctx *gin.Context, resource string, blockType base.BlockType) bool {
+	behavior, exist := fallback.GetWebFallbackBehavior(resource, blockType)
+	if !exist || behavior == nil {
+		ctx.AbortWithStatus(http.StatusTooManyRequests)
+		return false
+	}
+
+	switch behavior.WebFallbackMode {
+	case 0: // return
+		if behavior.WebRespContentType != 0 && behavior.WebRespContentType != 1 { // text
+			return false
+		}
+
+		if behavior.WebRespContentType == 0 { // text
+			ctx.String(int(behavior.WebRespStatusCode), behavior.WebRespMessage)
+			ctx.Abort()
+			return true
+		}
+		if behavior.WebRespContentType == 1 { // json
+			ctx.AbortWithStatusJSON(int(behavior.WebRespStatusCode), behavior.WebRespMessage)
+			return true
+		}
+	case 1: // redirect
+		ctx.Redirect(int(behavior.WebRespStatusCode), behavior.WebRedirectUrl)
+		return true
+	}
+	return false
+}
diff --git a/pkg/adapters/gin/middleware.go b/pkg/adapters/gin/middleware.go
index 1981950..c0ff92b 100644
--- a/pkg/adapters/gin/middleware.go
+++ b/pkg/adapters/gin/middleware.go
@@ -30,6 +30,8 @@ func SentinelMiddleware(opts ...Option) gin.HandlerFunc {
 		if err != nil {
 			if options.blockFallback != nil {
 				options.blockFallback(c)
+			} else if options.sentinelFallback != nil && !options.sentinelFallback(c, resourceName, err.BlockType()) {
+				c.AbortWithStatus(http.StatusTooManyRequests)
 			} else {
 				c.AbortWithStatus(http.StatusTooManyRequests)
 			}
diff --git a/pkg/adapters/gin/option.go b/pkg/adapters/gin/option.go
index afa1a6a..eb9847c 100644
--- a/pkg/adapters/gin/option.go
+++ b/pkg/adapters/gin/option.go
@@ -1,14 +1,16 @@
 package gin
 
 import (
+	"github.com/alibaba/sentinel-golang/core/base"
 	"github.com/gin-gonic/gin"
 )
 
 type (
 	Option  func(*options)
 	options struct {
-		resourceExtract func(*gin.Context) string
-		blockFallback   func(*gin.Context)
+		resourceExtract  func(*gin.Context) string
+		blockFallback    func(*gin.Context)
+		sentinelFallback func(*gin.Context, string, base.BlockType) bool
 	}
 )
 
@@ -34,3 +36,10 @@ func WithBlockFallback(fn func(ctx *gin.Context)) Option {
 		opts.blockFallback = fn
 	}
 }
+
+// WithSentinelFallback use sentinel fallback handler when requests are blocked
+func WithSentinelFallback() Option {
+	return func(opts *options) {
+		opts.sentinelFallback = sentinelFallback
+	}
+}
diff --git a/pkg/adapters/go-zero/fallback.go b/pkg/adapters/go-zero/fallback.go
new file mode 100644
index 0000000..29d6441
--- /dev/null
+++ b/pkg/adapters/go-zero/fallback.go
@@ -0,0 +1,42 @@
+package go_zero
+
+import (
+	"github.com/alibaba/sentinel-golang/core/base"
+	"github.com/alibaba/sentinel-golang/core/fallback"
+	"github.com/zeromicro/go-zero/rest/httpx"
+
+	//"github.com/zeromicro/go-zero/rest/httpx"
+	"net/http"
+)
+
+func sentinelFallback(w *http.ResponseWriter, req *http.Request, resource string, blockType base.BlockType) {
+	if w == nil || req == nil {
+		return
+	}
+
+	behavior, exist := fallback.GetWebFallbackBehavior(resource, blockType)
+	if !exist || behavior == nil {
+		http.Error(*w, "Blocked by Sentinel", http.StatusInternalServerError)
+		return
+	}
+
+	switch behavior.WebFallbackMode {
+	case 0: // return
+		if behavior.WebRespContentType != 0 && behavior.WebRespContentType != 1 { // text
+			http.Error(*w, "Blocked by Sentinel", http.StatusInternalServerError)
+			return
+		}
+
+		if behavior.WebRespContentType == 0 { // text
+			http.Error(*w, behavior.WebRespMessage, int(behavior.WebRespStatusCode))
+			return
+		}
+		if behavior.WebRespContentType == 1 { // json
+			httpx.WriteJson(*w, int(behavior.WebRespStatusCode), behavior.WebRespMessage)
+			return
+		}
+	case 1: // redirect
+		http.Redirect(*w, req, behavior.WebRedirectUrl, int(behavior.WebRespStatusCode))
+		return
+	}
+}
diff --git a/pkg/adapters/go-zero/global_middleware.go b/pkg/adapters/go-zero/global_middleware.go
index 4b940f2..e53147a 100644
--- a/pkg/adapters/go-zero/global_middleware.go
+++ b/pkg/adapters/go-zero/global_middleware.go
@@ -30,6 +30,8 @@ func SentinelMiddleware(opts ...Option) rest.Middleware {
 				if options.blockFallback != nil {
 					status, msg := options.blockFallback(r)
 					http.Error(w, msg, status)
+				} else if options.sentinelFallback != nil {
+					options.sentinelFallback(&w, r, resourceName, blockErr.BlockType())
 				} else {
 					// default error response
 					http.Error(w, "Blocked by Sentinel", http.StatusTooManyRequests)
diff --git a/pkg/adapters/go-zero/option.go b/pkg/adapters/go-zero/option.go
index 8cf7e01..f7ce434 100644
--- a/pkg/adapters/go-zero/option.go
+++ b/pkg/adapters/go-zero/option.go
@@ -1,12 +1,16 @@
 package go_zero
 
-import "net/http"
+import (
+	"github.com/alibaba/sentinel-golang/core/base"
+	"net/http"
+)
 
 type (
 	Option  func(*options)
 	options struct {
-		resourceExtract func(r *http.Request) string
-		blockFallback   func(r *http.Request) (int, string)
+		resourceExtract  func(r *http.Request) string
+		blockFallback    func(r *http.Request) (int, string)
+		sentinelFallback func(*http.ResponseWriter, *http.Request, string, base.BlockType)
 	}
 )
 
@@ -32,3 +36,10 @@ func WithBlockFallback(fn func(r *http.Request) (int, string)) Option {
 		opts.blockFallback = fn
 	}
 }
+
+// WithSentinelFallback use sentinel fallback handler when requests are blocked
+func WithSentinelFallback() Option {
+	return func(opts *options) {
+		opts.sentinelFallback = sentinelFallback
+	}
+}


Test patch:
diff --git a/core/fallback/rule_manager_test.go b/core/fallback/rule_manager_test.go
new file mode 100644
index 0000000..830c9e7
--- /dev/null
+++ b/core/fallback/rule_manager_test.go
@@ -0,0 +1,82 @@
+// Copyright 1999-2020 Alibaba Group Holding Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fallback
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestLoadRule(t *testing.T) {
+	_, err := LoadRules([]*Rule{
+		{
+			TargetResourceType: WebResourceType,
+			TargetMap: map[string][]FunctionType{
+				"/greet": {
+					FlowType,
+					Isolation,
+				},
+			},
+			FallbackBehavior: &WebBlockFallbackBehavior{
+				WebFallbackMode:    0,
+				WebRespContentType: 0,
+				WebRespStatusCode:  433,
+				WebRespMessage:     "1234599",
+			},
+		},
+		{
+			TargetResourceType: WebResourceType,
+			TargetMap: map[string][]FunctionType{
+				"/greet": {
+					HotspotHttp,
+				},
+			},
+			FallbackBehavior: &WebBlockFallbackBehavior{
+				WebFallbackMode:    0,
+				WebRespContentType: 1,
+				WebRespStatusCode:  434,
+				WebRespMessage:     "{\n  \"abc\": 123\n}",
+			},
+		},
+		{
+			TargetResourceType: WebResourceType,
+			TargetMap: map[string][]FunctionType{
+				"/api/users/:id": {
+					FlowType,
+				},
+			},
+			FallbackBehavior: &WebBlockFallbackBehavior{
+				WebFallbackMode:    0,
+				WebRespContentType: 1,
+				WebRespStatusCode:  434,
+				WebRespMessage:     "{\n  \"abc\": 123\n}",
+			},
+		},
+	})
+	assert.NoError(t, err)
+
+	assert.Equal(t, 2, len(webRuleMap))
+	assert.Equal(t, 0, len(rpcRuleMap))
+
+	funcTypeMap := webRuleMap["/greet"]
+	assert.Equal(t, 3, len(funcTypeMap))
+
+	assert.Equal(t, funcTypeMap[FlowType].WebRespStatusCode, int64(433))
+	assert.Equal(t, funcTypeMap[HotspotHttp].WebRespStatusCode, int64(434))
+
+	funcTypeMap = webRuleMap["/api/users/:id"]
+	assert.Equal(t, 1, len(funcTypeMap))
+
+}
diff --git a/pkg/adapters/gin/middleware_test.go b/pkg/adapters/gin/middleware_test.go
index 4371eb1..3fdaf20 100644
--- a/pkg/adapters/gin/middleware_test.go
+++ b/pkg/adapters/gin/middleware_test.go
@@ -1,6 +1,7 @@
 package gin
 
 import (
+	"github.com/alibaba/sentinel-golang/core/fallback"
 	"io"
 	"net/http"
 	"net/http/httptest"
@@ -18,6 +19,22 @@ func initSentinel(t *testing.T) {
 		t.Fatalf("Unexpected error: %+v", err)
 	}
 
+	_, err = fallback.LoadRules([]*fallback.Rule{
+		{
+			TargetResourceType: fallback.WebResourceType,
+			TargetMap: map[string][]fallback.FunctionType{
+				"/api/users/:id": {
+					fallback.FlowType,
+				},
+			},
+			FallbackBehavior: []byte("{\"webFallbackMode\":0,\"webRespContentType\":1,\"webRespMessage\":\"{\\n  \\\"abc\\\": 123\\n}\",\"webRespStatusCode\":400}"),
+		},
+	})
+	if err != nil {
+		t.Fatalf("Unexpected error: %+v", err)
+		return
+	}
+
 	_, err = flow.LoadRules([]*flow.Rule{
 		{
 			Resource:               "GET:/ping",
@@ -114,6 +131,27 @@ func TestSentinelMiddleware(t *testing.T) {
 					code: http.StatusBadRequest,
 				},
 			},
+			{
+				name: "sentinel fallback",
+				args: args{
+					opts: []Option{
+						WithResourceExtractor(func(ctx *gin.Context) string {
+							return ctx.FullPath()
+						}),
+						WithSentinelFallback(),
+					},
+					method:  http.MethodPost,
+					path:    "/api/users/:id",
+					reqPath: "/api/users/123",
+					handler: func(ctx *gin.Context) {
+						ctx.String(http.StatusOK, "ping")
+					},
+					body: nil,
+				},
+				want: want{
+					code: http.StatusBadRequest,
+				},
+			},
 		}
 	)
 	initSentinel(t)