fix(cloudflare): handle missing nodes in group detail responses

This commit is contained in:
ryan
2026-09-18 14:04:44 +08:00
parent a2404a50ef
commit 1d4533f183
3 changed files with 77 additions and 5 deletions
+1
View File
@@ -14,6 +14,7 @@ sidebar: false
- 控制台接入中英双语(next-intl,无 URL 语言前缀):默认中文,可在顶栏或「外观设置」切换;选择写入 cookie 后刷新生效。
### 🛠 修复
- 修复 Cloudflare 指向分组引用的节点已被删除时,分组列表/详情接口整体返回「Cloudflare 资源不存在」的问题;现会跳过缺失节点并继续返回其余分组。
- 修复静态导出部署下访问 Cloudflare 指向分组详情(`/cloudflare/groups/{id}`,id 不为 1)会跳回首页并触发 React hydration 报错的问题。
- 修复在网站列表中删除已加入 Cloudflare 指向分组的域名后,访问 Cloudflare 指向分组详情报错「Cloudflare 资源不存在」的问题。
- 修复自定义 Webhook 推送在企业微信/钉钉返回 HTTP 200 但 `errcode` 非零时仍记为成功的问题;任务日志会记录上游响应体。
+21 -5
View File
@@ -255,11 +255,11 @@ func validEdgeNode(ctx context.Context, id uint, requireIPv4 bool) (*model.OpenF
}
func buildGroupItem(ctx context.Context, group *model.CFPointingGroup) (*GroupItem, error) {
primary, err := repository.GetOpenFlareNodeByID(ctx, group.PrimaryNodeID)
primary, err := lookupGroupNode(ctx, group.ID, group.PrimaryNodeID)
if err != nil {
return nil, err
}
active, err := repository.GetOpenFlareNodeByID(ctx, group.ActiveNodeID)
active, err := lookupGroupNode(ctx, group.ID, group.ActiveNodeID)
if err != nil {
return nil, err
}
@@ -267,18 +267,34 @@ func buildGroupItem(ctx context.Context, group *model.CFPointingGroup) (*GroupIt
if err != nil {
return nil, err
}
item := &GroupItem{ID: group.ID, Name: group.Name, PrimaryNode: nodeOption(primary), ActiveNode: nodeOption(active), DefaultProxied: group.DefaultProxied, Enabled: group.Enabled, MemberCount: count, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt}
item := &GroupItem{ID: group.ID, Name: group.Name, PrimaryNode: nodeOptionForID(group.PrimaryNodeID, primary), ActiveNode: nodeOptionForID(group.ActiveNodeID, active), DefaultProxied: group.DefaultProxied, Enabled: group.Enabled, MemberCount: count, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt}
if group.BackupNodeID != nil {
backup, backupErr := repository.GetOpenFlareNodeByID(ctx, *group.BackupNodeID)
backup, backupErr := lookupGroupNode(ctx, group.ID, *group.BackupNodeID)
if backupErr != nil {
return nil, backupErr
}
option := nodeOption(backup)
option := nodeOptionForID(*group.BackupNodeID, backup)
item.BackupNode = &option
}
return item, nil
}
func lookupGroupNode(ctx context.Context, groupID, nodeID uint) (*model.OpenFlareNode, error) {
node, err := repository.GetOpenFlareNodeByID(ctx, nodeID)
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.WarnF(ctx, "[Cloudflare] pointing group references missing node: group_id=%d node_id=%d", groupID, nodeID)
return nil, nil
}
return node, err
}
func nodeOptionForID(id uint, node *model.OpenFlareNode) NodeOption {
if node == nil {
return NodeOption{ID: id}
}
return nodeOption(node)
}
func nodeOption(node *model.OpenFlareNode) NodeOption {
return NodeOption{ID: node.ID, Name: node.Name, IP: node.IP}
}
@@ -4,12 +4,14 @@
package cloudflare
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/shared/response"
"github.com/gin-gonic/gin"
@@ -75,3 +77,56 @@ func TestGetGroupWithOrphanedMemberHealsAndSucceeds(t *testing.T) {
t.Errorf("GetCFPointingMemberByID() should return not found after healing")
}
}
func TestListGroupsHandlerWithMissingNodeStillSucceeds(t *testing.T) {
ctx, _ := setupCloudflareLogicDB(t)
if err := db.DB(ctx).Create(&model.CFPointingGroup{
Name: "KR",
PrimaryNodeID: 12,
ActiveNodeID: 12,
Enabled: true,
}).Error; err != nil {
t.Fatalf("Create(missing-node group) error = %v", err)
}
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(response.ErrorHandlerMiddleware())
router.GET("/groups", ListGroupsHandler)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/groups", nil))
if recorder.Code != http.StatusOK {
t.Fatalf("ListGroupsHandler status = %d, body = %s, want %d", recorder.Code, recorder.Body.String(), http.StatusOK)
}
body := recorder.Body.String()
if !strings.Contains(body, `"name":"KR"`) {
t.Fatalf("ListGroupsHandler body = %s, want group KR", body)
}
if !strings.Contains(body, `"name":"primary"`) {
t.Fatalf("ListGroupsHandler body = %s, want intact group primary", body)
}
}
func TestGetGroupHandlerWithMissingNodeStillSucceeds(t *testing.T) {
ctx, _ := setupCloudflareLogicDB(t)
group := model.CFPointingGroup{Name: "KR", PrimaryNodeID: 12, ActiveNodeID: 12, Enabled: true}
if err := db.DB(ctx).Create(&group).Error; err != nil {
t.Fatalf("Create(missing-node group) error = %v", err)
}
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(response.ErrorHandlerMiddleware())
router.GET("/groups/:id", GetGroupHandler)
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/groups/%d", group.ID), nil)
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("GetGroupHandler status = %d, body = %s, want %d", recorder.Code, recorder.Body.String(), http.StatusOK)
}
if !strings.Contains(recorder.Body.String(), `"name":"KR"`) {
t.Fatalf("GetGroupHandler body = %s, want group KR", recorder.Body.String())
}
}