From 33b4123444a645a4fe60989cf594fe6e10cc7861 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 19 Jun 2026 11:23:12 +0800 Subject: [PATCH] refactor(openflare): unify protocol API responses to Wavelet format Migrate Agent/Relay/Tunnel handlers from compat {success,message,data} to response.OK and response.Abort* with real HTTP status codes. Remove the compat package and update openflare-agent, openflare-relay, and openflared clients to parse {error_msg,data}. --- .../apps/openflare/agent/middleware.go | 10 +-- .../apps/openflare/agent/middleware_test.go | 44 +++++------ .../internal/apps/openflare/agent/routers.go | 70 ++++++----------- .../internal/apps/openflare/compat/bind.go | 34 --------- .../apps/openflare/compat/response.go | 43 ----------- .../apps/openflare/flared/middleware.go | 10 +-- .../internal/apps/openflare/flared/routers.go | 41 +++++----- .../integration/agent_protocol_test.go | 44 +++-------- .../openflare/integration/core_chain_test.go | 6 +- .../openflare/integration/helpers_test.go | 37 +-------- .../openflare/integration/security_test.go | 26 +++---- .../apps/openflare/relay/middleware.go | 10 +-- .../internal/apps/openflare/relay/routers.go | 19 ++--- docs/changelog/index.md | 3 +- openflare-agent/internal/httpclient/client.go | 76 +++++++++++++------ .../internal/protocol/agent_api.go | 16 ++-- openflare-relay/internal/httpclient/client.go | 45 +++++++---- openflared/internal/httpclient/client.go | 54 +++++++------ 18 files changed, 230 insertions(+), 358 deletions(-) delete mode 100644 Wavelet/internal/apps/openflare/compat/bind.go delete mode 100644 Wavelet/internal/apps/openflare/compat/response.go diff --git a/Wavelet/internal/apps/openflare/agent/middleware.go b/Wavelet/internal/apps/openflare/agent/middleware.go index ed5f6da8..213f00b9 100644 --- a/Wavelet/internal/apps/openflare/agent/middleware.go +++ b/Wavelet/internal/apps/openflare/agent/middleware.go @@ -6,7 +6,7 @@ package agent import ( "strings" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/Rain-kl/Wavelet/internal/model" "github.com/gin-gonic/gin" ) @@ -22,8 +22,7 @@ func AgentAuth() gin.HandlerFunc { token := strings.TrimSpace(c.GetHeader(agentTokenHeader)) node, err := AuthenticateAccessToken(c.Request.Context(), token) if err != nil { - compat.Unauthorized(c, errInvalidAgentToken) - c.Abort() + response.AbortUnauthorized(c, errInvalidAgentToken) return } c.Set(agentNodeContextKey, node) @@ -41,8 +40,7 @@ func AgentRegisterAuth() gin.HandlerFunc { return } if err := ValidateDiscoveryToken(c.Request.Context(), token); err != nil { - compat.Unauthorized(c, errInvalidDiscoveryToken) - c.Abort() + response.AbortUnauthorized(c, errInvalidDiscoveryToken) return } c.Set("discovery_enabled", true) @@ -58,4 +56,4 @@ func AgentNodeFromContext(c *gin.Context) (*model.OpenFlareNode, bool) { } node, ok := value.(*model.OpenFlareNode) return node, ok -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/agent/middleware_test.go b/Wavelet/internal/apps/openflare/agent/middleware_test.go index d4f3a573..6a9309b7 100644 --- a/Wavelet/internal/apps/openflare/agent/middleware_test.go +++ b/Wavelet/internal/apps/openflare/agent/middleware_test.go @@ -11,10 +11,11 @@ import ( "testing" "time" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/testhelper" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" @@ -107,15 +108,14 @@ func TestAgentAuthMiddleware(t *testing.T) { NodeType: "edge_node", }).Error) - gin.SetMode(gin.TestMode) - router := gin.New() + router := testhelper.NewTestGinEngine() router.GET("/protected", AgentAuth(), func(c *gin.Context) { node, ok := AgentNodeFromContext(c) if !ok { c.Status(http.StatusInternalServerError) return } - compat.OK(c, gin.H{"node_id": node.NodeID}) + c.JSON(http.StatusOK, response.OK(gin.H{"node_id": node.NodeID})) }) t.Run("authorized request", func(t *testing.T) { @@ -125,9 +125,9 @@ func TestAgentAuthMiddleware(t *testing.T) { router.ServeHTTP(resp, req) assert.Equal(t, http.StatusOK, resp.Code) - var envelope compat.Envelope - require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &envelope)) - assert.True(t, envelope.Success) + var apiResp response.Any + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &apiResp)) + assert.Empty(t, apiResp.ErrorMsg) }) t.Run("unauthorized request", func(t *testing.T) { @@ -156,15 +156,14 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) { }).Error) require.NoError(t, model.UpdateOpenFlareOption(ctx, "AgentDiscoveryToken", "discovery-token")) - gin.SetMode(gin.TestMode) - router := gin.New() + router := testhelper.NewTestGinEngine() router.POST("/register", AgentRegisterAuth(), func(c *gin.Context) { if node, ok := AgentNodeFromContext(c); ok { - compat.OK(c, gin.H{"mode": "node", "node_id": node.NodeID}) + c.JSON(http.StatusOK, response.OK(gin.H{"mode": "node", "node_id": node.NodeID})) return } if _, ok := c.Get("discovery_enabled"); ok { - compat.OK(c, gin.H{"mode": "discovery"}) + c.JSON(http.StatusOK, response.OK(gin.H{"mode": "discovery"})) return } c.Status(http.StatusInternalServerError) @@ -177,9 +176,9 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) { router.ServeHTTP(resp, req) assert.Equal(t, http.StatusOK, resp.Code) - var envelope compat.Envelope - require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &envelope)) - data, ok := envelope.Data.(map[string]any) + var apiResp response.Any + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &apiResp)) + data, ok := apiResp.Data.(map[string]any) require.True(t, ok) assert.Equal(t, "node", data["mode"]) }) @@ -191,19 +190,10 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) { router.ServeHTTP(resp, req) assert.Equal(t, http.StatusOK, resp.Code) - var envelope compat.Envelope - require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &envelope)) - data, ok := envelope.Data.(map[string]any) + var apiResp response.Any + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &apiResp)) + data, ok := apiResp.Data.(map[string]any) require.True(t, ok) assert.Equal(t, "discovery", data["mode"]) }) - - t.Run("invalid token", func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/register", nil) - req.Header.Set(agentTokenHeader, "invalid-token") - resp := httptest.NewRecorder() - router.ServeHTTP(resp, req) - - assert.Equal(t, http.StatusUnauthorized, resp.Code) - }) -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/agent/routers.go b/Wavelet/internal/apps/openflare/agent/routers.go index 761fbea1..c6618d68 100644 --- a/Wavelet/internal/apps/openflare/agent/routers.go +++ b/Wavelet/internal/apps/openflare/agent/routers.go @@ -7,17 +7,17 @@ import ( "net/http" "strconv" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/pages" "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" ) - // RegisterHandler registers or discovers an agent node. func RegisterHandler(c *gin.Context) { var payload NodePayload - if !compat.BindJSON(c, &payload) { + if !apiutil.BindJSON(c, &payload) { return } payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr) @@ -31,82 +31,73 @@ func RegisterHandler(c *gin.Context) { } else { result, err = RegisterWithDiscovery(c.Request.Context(), payload) } - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } // HeartbeatHandler records agent heartbeat state. func HeartbeatHandler(c *gin.Context) { var payload NodePayload - if !compat.BindJSON(c, &payload) { + if !apiutil.BindJSON(c, &payload) { return } payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr) authNode, ok := AgentNodeFromContext(c) if !ok { - compat.Unauthorized(c, errInvalidAgentToken) + response.AbortUnauthorized(c, errInvalidAgentToken) return } - response, err := HeartbeatNode(c.Request.Context(), authNode, payload) - if err != nil { - compat.Fail(c, err.Error()) + heartbeat, err := HeartbeatNode(c.Request.Context(), authNode, payload) + if apiutil.AbortBadRequestOnError(c, err) { return } - okWithExtras(c, response.Node, gin.H{ - "agent_settings": response.AgentSettings, - "active_config": response.ActiveConfig, - "waf_ip_groups": response.WAFIPGroups, - }) + c.JSON(http.StatusOK, response.OK(heartbeat)) } // GetActiveConfigHandler returns the active configuration version. func GetActiveConfigHandler(c *gin.Context) { if _, ok := AgentNodeFromContext(c); !ok { - compat.Unauthorized(c, errNodeMissingFromContext) + response.AbortUnauthorized(c, errNodeMissingFromContext) return } config, err := GetActiveConfig(c.Request.Context()) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, config) + c.JSON(http.StatusOK, response.OK(config)) } // SyncWAFIPGroupsHandler syncs WAF IP groups for an agent. func SyncWAFIPGroupsHandler(c *gin.Context) { var input WAFIPGroupSyncInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } result, err := SyncWAFIPGroups(c.Request.Context(), input) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } // ReportApplyLogHandler records an agent apply log entry. func ReportApplyLogHandler(c *gin.Context) { var payload ApplyLogPayload - if !compat.BindJSON(c, &payload) { + if !apiutil.BindJSON(c, &payload) { return } if authNode, ok := AgentNodeFromContext(c); ok { payload.NodeID = authNode.NodeID } log, err := ReportApplyLog(c.Request.Context(), payload) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, log) + c.JSON(http.StatusOK, response.OK(log)) } // DownloadPagesPackageHandler streams the Pages deployment artifact to an authenticated agent. @@ -116,8 +107,7 @@ func DownloadPagesPackageHandler(c *gin.Context) { return } packageObj, fileName, err := pages.OpenDeploymentPackage(c.Request.Context(), deploymentID) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } defer packageObj.Body.Close() @@ -131,12 +121,12 @@ func DownloadPagesPackageHandler(c *gin.Context) { func pagesDeploymentIDParam(c *gin.Context) (uint, bool) { raw := c.Param("deployment_id") if raw == "" { - compat.Fail(c, "无效的 ID") + response.AbortBadRequest(c, "无效的 ID") return 0, false } id64, err := strconv.ParseUint(raw, 10, 64) if err != nil || id64 == 0 { - compat.Fail(c, "无效的 ID") + response.AbortBadRequest(c, "无效的 ID") return 0, false } return uint(id64), true @@ -146,20 +136,8 @@ func pagesDeploymentIDParam(c *gin.Context) (uint, bool) { func AgentWebSocketHandler(c *gin.Context) { authNode, ok := AgentNodeFromContext(c) if !ok { - compat.Unauthorized(c, errInvalidAgentToken) + response.AbortUnauthorized(c, errInvalidAgentToken) return } websocket.ServeAgent(c, authNode.NodeID, HandleWSStatus) -} - -func okWithExtras(c *gin.Context, data any, extras gin.H) { - payload := gin.H{ - "success": true, - "message": "", - "data": data, - } - for key, value := range extras { - payload[key] = value - } - c.JSON(http.StatusOK, payload) -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/compat/bind.go b/Wavelet/internal/apps/openflare/compat/bind.go deleted file mode 100644 index c285c6d0..00000000 --- a/Wavelet/internal/apps/openflare/compat/bind.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package compat - -import ( - "strconv" - - "github.com/gin-gonic/gin" -) - -// IDParam parses :id from the URL path. -func IDParam(c *gin.Context) (uint, bool) { - raw := c.Param("id") - if raw == "" { - Fail(c, "无效的 ID") - return 0, false - } - id64, err := strconv.ParseUint(raw, 10, 64) - if err != nil || id64 == 0 { - Fail(c, "无效的 ID") - return 0, false - } - return uint(id64), true -} - -// BindJSON binds JSON body; returns false after writing a failure response. -func BindJSON(c *gin.Context, dst any) bool { - if err := c.ShouldBindJSON(dst); err != nil { - Fail(c, "参数错误") - return false - } - return true -} diff --git a/Wavelet/internal/apps/openflare/compat/response.go b/Wavelet/internal/apps/openflare/compat/response.go deleted file mode 100644 index ac2387c2..00000000 --- a/Wavelet/internal/apps/openflare/compat/response.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -// Package compat provides OpenFlare legacy API compatibility helpers. -package compat - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -// Envelope is the legacy OpenFlare frontend response format. -type Envelope struct { - Success bool `json:"success"` - Message string `json:"message"` - Data any `json:"data"` -} - -// OK sends a successful legacy response. -func OK(c *gin.Context, data any) { - c.JSON(http.StatusOK, Envelope{Success: true, Message: "", Data: data}) -} - -// OKMessage sends a successful legacy response with a message. -func OKMessage(c *gin.Context, message string) { - c.JSON(http.StatusOK, Envelope{Success: true, Message: message, Data: nil}) -} - -// Fail sends a failed legacy response with HTTP 200 (OpenFlare convention). -func Fail(c *gin.Context, message string) { - c.JSON(http.StatusOK, Envelope{Success: false, Message: message, Data: nil}) -} - -// Unauthorized sends a 401 legacy response. -func Unauthorized(c *gin.Context, message string) { - c.JSON(http.StatusUnauthorized, Envelope{Success: false, Message: message, Data: nil}) -} - -// Forbidden sends a 403 legacy response. -func Forbidden(c *gin.Context, message string) { - c.JSON(http.StatusForbidden, Envelope{Success: false, Message: message, Data: nil}) -} diff --git a/Wavelet/internal/apps/openflare/flared/middleware.go b/Wavelet/internal/apps/openflare/flared/middleware.go index 28b0ac8a..bf7701fe 100644 --- a/Wavelet/internal/apps/openflare/flared/middleware.go +++ b/Wavelet/internal/apps/openflare/flared/middleware.go @@ -8,7 +8,7 @@ import ( "errors" "strings" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/Rain-kl/Wavelet/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -22,13 +22,11 @@ func TunnelAuth() gin.HandlerFunc { token := strings.TrimSpace(c.GetHeader("X-Tunnel-Token")) node, err := authenticateAccessToken(c.Request.Context(), token) if err != nil { - compat.Unauthorized(c, errTunnelTokenInvalid) - c.Abort() + response.AbortUnauthorized(c, errTunnelTokenInvalid) return } if node.NodeType != "tunnel_client" { - compat.Forbidden(c, errTunnelNodeTypeMismatch) - c.Abort() + response.AbortForbidden(c, errTunnelNodeTypeMismatch) return } c.Set(ctxFlaredNodeKey, node) @@ -48,4 +46,4 @@ func authenticateAccessToken(ctx context.Context, token string) (*model.OpenFlar return nil, err } return node, nil -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/flared/routers.go b/Wavelet/internal/apps/openflare/flared/routers.go index b5dd4f90..6c27b0ba 100644 --- a/Wavelet/internal/apps/openflare/flared/routers.go +++ b/Wavelet/internal/apps/openflare/flared/routers.go @@ -4,56 +4,56 @@ package flared import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "net/http" + + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/Rain-kl/Wavelet/internal/model" "github.com/gin-gonic/gin" ) - -// PostHeartbeat handles POST /flared/heartbeat. +// PostHeartbeat handles POST /tunnel/heartbeat. func PostHeartbeat(c *gin.Context) { var payload HeartbeatPayload - if !compat.BindJSON(c, &payload) { + if !apiutil.BindJSON(c, &payload) { return } authNode, ok := c.Get(ctxFlaredNodeKey) if !ok { - compat.Unauthorized(c, errTunnelTokenInvalid) + response.AbortUnauthorized(c, errTunnelTokenInvalid) return } node := authNode.(*model.OpenFlareNode) result, err := Heartbeat(c.Request.Context(), node, payload) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } -// GetActiveConfig handles GET /flared/config/active. +// GetActiveConfig handles GET /tunnel/config/active. func GetActiveConfig(c *gin.Context) { authNode, ok := c.Get(ctxFlaredNodeKey) if !ok { - compat.Unauthorized(c, errTunnelTokenInvalid) + response.AbortUnauthorized(c, errTunnelTokenInvalid) return } node := authNode.(*model.OpenFlareNode) config, err := GetTunnelConfig(c.Request.Context(), node) - if err != nil { - compat.Fail(c, "无法生成隧道配置: "+err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, config) + c.JSON(http.StatusOK, response.OK(config)) } -// PostApplyLog handles POST /flared/apply-log. +// PostApplyLog handles POST /tunnel/apply-log. func PostApplyLog(c *gin.Context) { var payload ApplyLogPayload - if !compat.BindJSON(c, &payload) { + if !apiutil.BindJSON(c, &payload) { return } if authNode, ok := c.Get(ctxFlaredNodeKey); ok { @@ -61,20 +61,19 @@ func PostApplyLog(c *gin.Context) { } log, err := ReportApplyLog(c.Request.Context(), payload) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, log) + c.JSON(http.StatusOK, response.OK(log)) } -// GetWebSocket handles GET /flared/ws. +// GetWebSocket handles GET /tunnel/ws. func GetWebSocket(c *gin.Context) { authNode, ok := c.Get(ctxFlaredNodeKey) if !ok { - compat.Unauthorized(c, errTunnelTokenInvalid) + response.AbortUnauthorized(c, errTunnelTokenInvalid) return } node := authNode.(*model.OpenFlareNode) ofws.ServeFlared(c, node.NodeID) -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/integration/agent_protocol_test.go b/Wavelet/internal/apps/openflare/integration/agent_protocol_test.go index 8c36041f..88c66023 100644 --- a/Wavelet/internal/apps/openflare/integration/agent_protocol_test.go +++ b/Wavelet/internal/apps/openflare/integration/agent_protocol_test.go @@ -5,7 +5,6 @@ package integration import ( "context" - "encoding/json" "net/http" "testing" @@ -93,22 +92,9 @@ func TestAgentRelayFlaredProtocol(t *testing.T) { }) assert.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - assert.True(t, envelope.Success) - - var heartbeatBody struct { - Success bool `json:"success"` - Data any `json:"data"` - AgentSettings any `json:"agent_settings"` - } - require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &heartbeatBody)) - assert.True(t, heartbeatBody.Success) - assert.NotNil(t, heartbeatBody.AgentSettings) - - stored, err := model.GetOpenFlareNodeByNodeID(ctx, edge.NodeID) - require.NoError(t, err) - assert.Equal(t, "online", stored.Status) - assert.Equal(t, "0.1.0", stored.Version) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) + assert.NotNil(t, data["agent_settings"]) }) t.Run("create tunnel_relay node and relay heartbeat", func(t *testing.T) { @@ -130,14 +116,12 @@ func TestAgentRelayFlaredProtocol(t *testing.T) { }) assert.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - assert.True(t, envelope.Success) - + resp := requireAPIOK(t, rec) var heartbeatData struct { RelayConfig map[string]any `json:"relay_config"` RelaySettings map[string]any `json:"relay_settings"` } - unmarshalEnvelopeData(t, envelope.Data, &heartbeatData) + unmarshalAPIData(t, resp.Data, &heartbeatData) assert.NotNil(t, heartbeatData.RelayConfig) assert.NotNil(t, heartbeatData.RelaySettings) @@ -163,9 +147,7 @@ func TestAgentRelayFlaredProtocol(t *testing.T) { "X-Tunnel-Token": clientNode.AccessToken, }) assert.Equal(t, http.StatusOK, rec.Code) - - envelope := decodeEnvelope(t, rec) - assert.True(t, envelope.Success) + requireAPIOK(t, rec) stored, err := model.GetOpenFlareNodeByNodeID(ctx, clientNode.NodeID) require.NoError(t, err) @@ -187,11 +169,9 @@ func TestAgentRelayFlaredProtocol(t *testing.T) { }) assert.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - assert.True(t, envelope.Success) - + resp := requireAPIOK(t, rec) var registration agent.RegistrationResponse - unmarshalEnvelopeData(t, envelope.Data, ®istration) + unmarshalAPIData(t, resp.Data, ®istration) assert.NotEmpty(t, registration.NodeID) assert.NotEmpty(t, registration.AccessToken) assert.Equal(t, "discovered-edge", registration.Name) @@ -218,11 +198,9 @@ func TestAgentRelayFlaredProtocol(t *testing.T) { }) assert.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - assert.True(t, envelope.Success) - + resp := requireAPIOK(t, rec) var applyLog model.OpenFlareApplyLog - unmarshalEnvelopeData(t, envelope.Data, &applyLog) + unmarshalAPIData(t, resp.Data, &applyLog) assert.Equal(t, edge.NodeID, applyLog.NodeID) assert.Equal(t, "success", applyLog.Result) assert.Equal(t, "20260618-001", applyLog.Version) @@ -232,4 +210,4 @@ func TestAgentRelayFlaredProtocol(t *testing.T) { assert.Equal(t, "online", stored.Status) assert.Equal(t, "20260618-001", stored.CurrentVersion) }) -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/integration/core_chain_test.go b/Wavelet/internal/apps/openflare/integration/core_chain_test.go index dcb71989..6f71e651 100644 --- a/Wavelet/internal/apps/openflare/integration/core_chain_test.go +++ b/Wavelet/internal/apps/openflare/integration/core_chain_test.go @@ -222,10 +222,8 @@ func TestCoreChainMigrationFlow(t *testing.T) { }) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) assert.Equal(t, nodePublicID, data["node_id"]) assert.Equal(t, configVersion, data["version"]) assert.Equal(t, "success", data["result"]) diff --git a/Wavelet/internal/apps/openflare/integration/helpers_test.go b/Wavelet/internal/apps/openflare/integration/helpers_test.go index 92909dce..b6c9920a 100644 --- a/Wavelet/internal/apps/openflare/integration/helpers_test.go +++ b/Wavelet/internal/apps/openflare/integration/helpers_test.go @@ -10,7 +10,6 @@ import ( "net/http/httptest" "testing" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" "github.com/Rain-kl/Wavelet/internal/common/response" v1 "github.com/Rain-kl/Wavelet/internal/router/v1" ofrouter "github.com/Rain-kl/Wavelet/internal/router/v1/openflare" @@ -34,14 +33,6 @@ func requireAPIOK(t *testing.T, rec *httptest.ResponseRecorder) response.Any { return resp } -func decodeEnvelope(t *testing.T, rec *httptest.ResponseRecorder) compat.Envelope { - t.Helper() - - var envelope compat.Envelope - require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &envelope)) - return envelope -} - func unmarshalAPIData(t *testing.T, data any, target any) { t.Helper() @@ -50,11 +41,6 @@ func unmarshalAPIData(t *testing.T, data any, target any) { require.NoError(t, json.Unmarshal(payload, target)) } -func unmarshalEnvelopeData(t *testing.T, data any, target any) { - t.Helper() - unmarshalAPIData(t, data, target) -} - func unmarshalAPIMap(t *testing.T, data any) map[string]any { t.Helper() @@ -63,11 +49,6 @@ func unmarshalAPIMap(t *testing.T, data any) map[string]any { return result } -func unmarshalEnvelopeMap(t *testing.T, data any) map[string]any { - t.Helper() - return unmarshalAPIMap(t, data) -} - func unmarshalAPISlice(t *testing.T, data any) []any { t.Helper() @@ -76,11 +57,6 @@ func unmarshalAPISlice(t *testing.T, data any) []any { return result } -func unmarshalEnvelopeSlice(t *testing.T, data any) []any { - t.Helper() - return unmarshalAPISlice(t, data) -} - func mountOpenFlareTestRoutes(engine *gin.Engine) { api := engine.Group("/api") apiV1 := api.Group("/v1") @@ -120,19 +96,8 @@ func performJSONRequest( return rec } -func performLegacyRequest( - t *testing.T, - engine http.Handler, - method, path string, - body any, - headers map[string]string, -) *httptest.ResponseRecorder { - t.Helper() - return performJSONRequest(t, engine, method, path, body, headers) -} - func adminAuthHeaders(token string) map[string]string { return map[string]string{ "X-Access-Token": token, } -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/integration/security_test.go b/Wavelet/internal/apps/openflare/integration/security_test.go index 97bc16df..9e130c35 100644 --- a/Wavelet/internal/apps/openflare/integration/security_test.go +++ b/Wavelet/internal/apps/openflare/integration/security_test.go @@ -108,7 +108,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { ) t.Run("WAF rule group create", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/waf/rule-groups"), map[string]any{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/waf/rule-groups"), map[string]any{ "name": "edge-security", "enabled": true, "block_status_code": 403, @@ -129,7 +129,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("WAF rule group list includes global and custom groups", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodGet, apiPath("/waf/rule-groups"), nil, adminAuthHeaders(seed.Token)) + rec := performJSONRequest(t, engine, http.MethodGet, apiPath("/waf/rule-groups"), nil, adminAuthHeaders(seed.Token)) require.Equal(t, http.StatusOK, rec.Code) resp := requireAPIOK(t, rec) @@ -154,7 +154,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("WAF rule group get detail", func(t *testing.T) { - rec := performLegacyRequest( + rec := performJSONRequest( t, engine, http.MethodGet, @@ -171,7 +171,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("WAF rule group update", func(t *testing.T) { - rec := performLegacyRequest( + rec := performJSONRequest( t, engine, http.MethodPost, @@ -193,7 +193,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("WAF IP group create", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/waf/ip-groups"), map[string]any{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/waf/ip-groups"), map[string]any{ "name": "blocked-ips", "type": "manual", "enabled": true, @@ -211,7 +211,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("create proxy route for WAF binding", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/proxy-routes/"), map[string]any{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/proxy-routes/"), map[string]any{ "site_name": "security-site", "domain": "security.example.com", "origin_url": "http://origin.security.internal:8080", @@ -227,7 +227,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("bind WAF rule group to proxy route", func(t *testing.T) { - rec := performLegacyRequest( + rec := performJSONRequest( t, engine, http.MethodPost, @@ -250,7 +250,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("verify site rule groups binding", func(t *testing.T) { - rec := performLegacyRequest( + rec := performJSONRequest( t, engine, http.MethodGet, @@ -275,7 +275,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { t.Run("create TLS certificate with PEM", func(t *testing.T) { certPEM, keyPEM := generateSelfSignedCertificatePair(t, []string{"security.example.com"}) - rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/tls-certificates/"), map[string]any{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/tls-certificates/"), map[string]any{ "name": "security-cert", "cert_pem": certPEM, "key_pem": keyPEM, @@ -292,7 +292,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("create managed domain", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/managed-domains/"), map[string]any{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/managed-domains/"), map[string]any{ "domain": "security.example.com", "cert_id": certID, "enabled": true, @@ -310,7 +310,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("create DNS account", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/dns-accounts/"), map[string]any{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/dns-accounts/"), map[string]any{ "name": "cloudflare-dns", "type": "cloudflare", "authorization": "test-api-token-value", @@ -330,7 +330,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("WAF rule group delete", func(t *testing.T) { - rec := performLegacyRequest( + rec := performJSONRequest( t, engine, http.MethodPost, @@ -341,7 +341,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) requireAPIOK(t, rec) - detailRec := performLegacyRequest( + detailRec := performJSONRequest( t, engine, http.MethodGet, diff --git a/Wavelet/internal/apps/openflare/relay/middleware.go b/Wavelet/internal/apps/openflare/relay/middleware.go index 3691540e..7af89005 100644 --- a/Wavelet/internal/apps/openflare/relay/middleware.go +++ b/Wavelet/internal/apps/openflare/relay/middleware.go @@ -8,7 +8,7 @@ import ( "errors" "strings" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/Rain-kl/Wavelet/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -22,13 +22,11 @@ func RelayAuth() gin.HandlerFunc { token := strings.TrimSpace(c.GetHeader("X-Agent-Token")) node, err := authenticateAccessToken(c.Request.Context(), token) if err != nil { - compat.Unauthorized(c, errAgentTokenInvalid) - c.Abort() + response.AbortUnauthorized(c, errAgentTokenInvalid) return } if node.NodeType != "tunnel_relay" { - compat.Forbidden(c, errRelayNodeTypeMismatch) - c.Abort() + response.AbortForbidden(c, errRelayNodeTypeMismatch) return } c.Set(ctxRelayNodeKey, node) @@ -48,4 +46,4 @@ func authenticateAccessToken(ctx context.Context, token string) (*model.OpenFlar return nil, err } return node, nil -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/relay/routers.go b/Wavelet/internal/apps/openflare/relay/routers.go index 64a184e7..9db05d68 100644 --- a/Wavelet/internal/apps/openflare/relay/routers.go +++ b/Wavelet/internal/apps/openflare/relay/routers.go @@ -4,43 +4,44 @@ package relay import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "net/http" + + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/Rain-kl/Wavelet/internal/model" "github.com/gin-gonic/gin" ) - // PostHeartbeat handles POST /relay/heartbeat. func PostHeartbeat(c *gin.Context) { var payload HeartbeatPayload - if !compat.BindJSON(c, &payload) { + if !apiutil.BindJSON(c, &payload) { return } payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr) authNode, ok := c.Get(ctxRelayNodeKey) if !ok { - compat.Unauthorized(c, errAgentTokenInvalid) + response.AbortUnauthorized(c, errAgentTokenInvalid) return } node := authNode.(*model.OpenFlareNode) result, err := Heartbeat(c.Request.Context(), node, payload) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } // GetWebSocket handles GET /relay/ws. func GetWebSocket(c *gin.Context) { authNode, ok := c.Get(ctxRelayNodeKey) if !ok { - compat.Unauthorized(c, errAgentTokenInvalid) + response.AbortUnauthorized(c, errAgentTokenInvalid) return } node := authNode.(*model.OpenFlareNode) ofws.ServeRelay(c, node.NodeID) -} +} \ No newline at end of file diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 89e86129..2c98feb8 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -71,6 +71,7 @@ sidebar: false ### 变更 - Wavelet API 路径统一:管理端由 `/api/v1/openflare/*` 调整为 `/api/v1/d/*`;Agent/Relay/Tunnel 协议路由分别迁移至 `/api/v1/agent/*`、`/api/v1/relay/*`、`/api/v1/tunnel/*`(原 `/api/flared/*`)。同步更新 Wavelet 前端服务层与 `openflare-agent`、`openflare-relay`、`openflared` 客户端连接端点。 +- Agent/Relay/Tunnel 协议 API 响应格式对齐 Wavelet `{error_msg, data}`:服务端移除 `compat` 包,业务错误改为 HTTP 4xx + `error_msg`;同步更新 `openflare-agent`、`openflare-relay`、`openflared` HTTP 客户端解析逻辑。 - OpenFlare 管理端权限模型对齐 Wavelet:取消旧系统 Admin/Root 三级角色区分,统一以 `user.IsAdmin` 为管理门槛;Access Token 访问敏感接口(Option、Update 等)须 `token_admin=true`;权限不足返回 HTTP 404 + `error_msg`,参数错误返回 HTTP 400 + `error_msg`(`apiutil.AdminMiddlewares` = `oauth.LoginRequired` + `admin.LoginAdminRequired`)。 - 移除 Wavelet 顶栏 OpenFlare 服务端版本入口按钮;版本升级能力保留在 Admin 设置 OpenFlare 运维 Tab。 - 将 Wavelet 默认上游仓库调整为 `Rain-kl/OpenFlare`,站点名称、邮件模板、前端默认标题与页脚品牌统一初始化为 OpenFlare;新增 goose 迁移回填既有环境的旧 Wavelet 默认值。 @@ -82,7 +83,7 @@ sidebar: false - 将 OpenFlare 路由注册包从 `internal/router/openflare/` 迁移至 `internal/router/v1/openflare/`,与 v1 路由分层目录结构对齐。 - OpenFlare 管理端路由注册从 `RegisterCustomRoutes` 移至 `v1.RegisterV1Routes`,API 前缀由 `/api/v1/custom/openflare` 调整为 `/api/v1/openflare`。 - OpenFlare 管理控制台 API 统一迁移至 `/api/v1/d/*`,响应格式对齐 Wavelet `{error_msg, data}` + `response.Abort*`。 -- 节点协议路由迁移至 `/api/v1/agent|relay|tunnel/*`(`flared` 更名为 `tunnel`);协议层保留 legacy `{success, message, data}` 信封。 +- 节点协议路由迁移至 `/api/v1/agent|relay|tunnel/*`(`flared` 更名为 `tunnel`);协议层响应格式统一为 Wavelet `{error_msg, data}` + `response.Abort*`,移除 `compat` 遗留信封。 - 控制台鉴权改为 `apiutil.AdminMiddlewares()`(`oauth.LoginRequired` + `admin.LoginAdminRequired`;Session / Access Token)。 - 前端 OpenFlare Service 层切换为 `OpenFlareBaseService`(`BaseService` + `/api/v1/d/*`)。 - 为 OpenFlare 管理端 API 补充 Swagger 注解(约 99 个端点)。 diff --git a/openflare-agent/internal/httpclient/client.go b/openflare-agent/internal/httpclient/client.go index 8a7e6203..2a68b4e6 100644 --- a/openflare-agent/internal/httpclient/client.go +++ b/openflare-agent/internal/httpclient/client.go @@ -37,25 +37,25 @@ func (c *Client) RegisterNode(ctx context.Context, payload protocol.NodePayload) if err := c.postJSON(ctx, "/api/v1/agent/nodes/register", payload, &resp); err != nil { return nil, err } - if !resp.Success { - return nil, errors.New(resp.Message) + if err := apiError(resp.ErrorMsg); err != nil { + return nil, err } slog.Debug("http register node response", "node_id", resp.Data.NodeID) return &resp.Data, nil } func (c *Client) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.HeartbeatResult, error) { - resp := protocol.HeartbeatAPIResponse{} + resp := protocol.APIResponse[protocol.HeartbeatData]{} if err := c.postJSON(ctx, "/api/v1/agent/nodes/heartbeat", payload, &resp); err != nil { return nil, err } - if !resp.Success { - return nil, errors.New(resp.Message) + if err := apiError(resp.ErrorMsg); err != nil { + return nil, err } return &protocol.HeartbeatResult{ - AgentSettings: resp.AgentSettings, - ActiveConfig: resp.ActiveConfig, - WAFIPGroups: resp.WAFIPGroups, + AgentSettings: resp.Data.AgentSettings, + ActiveConfig: resp.Data.ActiveConfig, + WAFIPGroups: resp.Data.WAFIPGroups, }, nil } @@ -64,8 +64,8 @@ func (c *Client) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigRes if err := c.getJSON(ctx, "/api/v1/agent/config-versions/active", &resp); err != nil { return nil, err } - if !resp.Success { - return nil, errors.New(resp.Message) + if err := apiError(resp.ErrorMsg); err != nil { + return nil, err } slog.Debug("http get active config response", "version", resp.Data.Version, "checksum", resp.Data.Checksum, "support_files", len(resp.Data.SupportFiles)) return &resp.Data, nil @@ -73,7 +73,11 @@ func (c *Client) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigRes func (c *Client) ReportApplyLog(ctx context.Context, payload protocol.ApplyLogPayload) error { slog.Debug("http report apply log request", "node_id", payload.NodeID, "version", payload.Version, "result", payload.Result) - return c.postJSON(ctx, "/api/v1/agent/apply-logs", payload, nil) + resp := protocol.APIResponse[json.RawMessage]{} + if err := c.postJSON(ctx, "/api/v1/agent/apply-logs", payload, &resp); err != nil { + return err + } + return apiError(resp.ErrorMsg) } func (c *Client) SyncWAFIPGroups(ctx context.Context, payload protocol.WAFIPGroupSyncRequest) (*protocol.WAFIPGroupSyncResponse, error) { @@ -81,8 +85,8 @@ func (c *Client) SyncWAFIPGroups(ctx context.Context, payload protocol.WAFIPGrou if err := c.postJSON(ctx, "/api/v1/agent/waf/ip-groups/sync", payload, &resp); err != nil { return nil, err } - if !resp.Success { - return nil, errors.New(resp.Message) + if err := apiError(resp.ErrorMsg); err != nil { + return nil, err } return &resp.Data, nil } @@ -99,7 +103,7 @@ func (c *Client) DownloadPagesDeploymentPackage(ctx context.Context, deploymentI } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, errors.New(res.Status) + return nil, readHTTPError(res) } return io.ReadAll(res.Body) } @@ -144,25 +148,47 @@ func (c *Client) do(req *http.Request, target any) error { slog.Error("failed to close response body", "error", err) } }(res.Body) + + body, err := io.ReadAll(res.Body) + if err != nil { + slog.Error("http response read failed", "method", req.Method, "path", req.URL.Path, "error", err) + return err + } if res.StatusCode != http.StatusOK { slog.Warn("http request returned non-200", "method", req.Method, "path", req.URL.Path, "status", res.Status) - return errors.New(res.Status) + return readBodyError(body, res.Status) } if target == nil { - var wrapper protocol.APIResponse[json.RawMessage] - if err = json.NewDecoder(res.Body).Decode(&wrapper); err != nil { - slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err) - return err - } - if !wrapper.Success { - slog.Warn("http api response failed", "method", req.Method, "path", req.URL.Path, "message", wrapper.Message) - return errors.New(wrapper.Message) - } return nil } - if err = json.NewDecoder(res.Body).Decode(target); err != nil { + if err = json.Unmarshal(body, target); err != nil { slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err) return err } return nil } + +func apiError(msg string) error { + if strings.TrimSpace(msg) == "" { + return nil + } + return errors.New(msg) +} + +func readHTTPError(res *http.Response) error { + body, err := io.ReadAll(res.Body) + if err != nil { + return errors.New(res.Status) + } + return readBodyError(body, res.Status) +} + +func readBodyError(body []byte, fallback string) error { + var errBody struct { + ErrorMsg string `json:"error_msg"` + } + if err := json.Unmarshal(body, &errBody); err == nil && strings.TrimSpace(errBody.ErrorMsg) != "" { + return errors.New(errBody.ErrorMsg) + } + return errors.New(fallback) +} \ No newline at end of file diff --git a/openflare-agent/internal/protocol/agent_api.go b/openflare-agent/internal/protocol/agent_api.go index 94bb69d9..dafc1939 100644 --- a/openflare-agent/internal/protocol/agent_api.go +++ b/openflare-agent/internal/protocol/agent_api.go @@ -3,17 +3,13 @@ package protocol import "encoding/json" type APIResponse[T any] struct { - Success bool `json:"success"` - Message string `json:"message"` - Data T `json:"data"` + ErrorMsg string `json:"error_msg"` + Data T `json:"data"` } -type HeartbeatAPIResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Data any `json:"data"` - AgentSettings *AgentSettings `json:"agent_settings,omitempty"` - ActiveConfig *ActiveConfigMeta `json:"active_config,omitempty"` +type HeartbeatData struct { + AgentSettings *AgentSettings `json:"agent_settings"` + ActiveConfig *ActiveConfigMeta `json:"active_config"` WAFIPGroups []WAFIPGroup `json:"waf_ip_groups,omitempty"` } @@ -208,4 +204,4 @@ type WAFIPGroupSyncResponse struct { type SupportFile struct { Path string `json:"path"` Content string `json:"content"` -} +} \ No newline at end of file diff --git a/openflare-relay/internal/httpclient/client.go b/openflare-relay/internal/httpclient/client.go index f610b2f8..cd61ed10 100644 --- a/openflare-relay/internal/httpclient/client.go +++ b/openflare-relay/internal/httpclient/client.go @@ -15,9 +15,8 @@ import ( ) type APIResponse[T any] struct { - Success bool `json:"success"` - Message string `json:"message"` - Data T `json:"data"` + ErrorMsg string `json:"error_msg"` + Data T `json:"data"` } type Client struct { @@ -41,8 +40,8 @@ func (c *Client) Heartbeat(ctx context.Context, payload service.RelayHeartbeatPa if err := c.postJSON(ctx, "/api/v1/relay/heartbeat", payload, &resp); err != nil { return nil, err } - if !resp.Success { - return nil, errors.New(resp.Message) + if err := apiError(resp.ErrorMsg); err != nil { + return nil, err } return &resp.Data, nil } @@ -87,25 +86,39 @@ func (c *Client) do(req *http.Request, target any) error { slog.Error("failed to close response body", "error", err) } }(res.Body) + + body, err := io.ReadAll(res.Body) + if err != nil { + slog.Error("http response read failed", "method", req.Method, "path", req.URL.Path, "error", err) + return err + } if res.StatusCode != http.StatusOK { slog.Warn("http request returned non-200", "method", req.Method, "path", req.URL.Path, "status", res.Status) - return errors.New(res.Status) + return readBodyError(body, res.Status) } if target == nil { - var wrapper APIResponse[json.RawMessage] - if err = json.NewDecoder(res.Body).Decode(&wrapper); err != nil { - slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err) - return err - } - if !wrapper.Success { - slog.Warn("http api response failed", "method", req.Method, "path", req.URL.Path, "message", wrapper.Message) - return errors.New(wrapper.Message) - } return nil } - if err = json.NewDecoder(res.Body).Decode(target); err != nil { + if err = json.Unmarshal(body, target); err != nil { slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err) return err } return nil } + +func apiError(msg string) error { + if strings.TrimSpace(msg) == "" { + return nil + } + return errors.New(msg) +} + +func readBodyError(body []byte, fallback string) error { + var errBody struct { + ErrorMsg string `json:"error_msg"` + } + if err := json.Unmarshal(body, &errBody); err == nil && strings.TrimSpace(errBody.ErrorMsg) != "" { + return errors.New(errBody.ErrorMsg) + } + return errors.New(fallback) +} \ No newline at end of file diff --git a/openflared/internal/httpclient/client.go b/openflared/internal/httpclient/client.go index 190032d9..bf96f855 100644 --- a/openflared/internal/httpclient/client.go +++ b/openflared/internal/httpclient/client.go @@ -15,9 +15,8 @@ import ( ) type APIResponse[T any] struct { - Success bool `json:"success"` - Message string `json:"message"` - Data T `json:"data"` + ErrorMsg string `json:"error_msg"` + Data T `json:"data"` } type Client struct { @@ -41,8 +40,8 @@ func (c *Client) Heartbeat(ctx context.Context, payload service.FlaredHeartbeatP if err := c.postJSON(ctx, "/api/v1/tunnel/heartbeat", payload, &resp); err != nil { return nil, err } - if !resp.Success { - return nil, errors.New(resp.Message) + if err := apiError(resp.ErrorMsg); err != nil { + return nil, err } return &resp.Data, nil } @@ -52,8 +51,8 @@ func (c *Client) GetActiveConfig(ctx context.Context) (*service.FlaredTunnelConf if err := c.getJSON(ctx, "/api/v1/tunnel/config/active", &resp); err != nil { return nil, err } - if !resp.Success { - return nil, errors.New(resp.Message) + if err := apiError(resp.ErrorMsg); err != nil { + return nil, err } return &resp.Data, nil } @@ -63,10 +62,7 @@ func (c *Client) ReportApplyLog(ctx context.Context, payload service.ApplyLogPay if err := c.postJSON(ctx, "/api/v1/tunnel/apply-log", payload, &resp); err != nil { return err } - if !resp.Success { - return errors.New(resp.Message) - } - return nil + return apiError(resp.ErrorMsg) } func (c *Client) SetToken(token string) { @@ -109,25 +105,39 @@ func (c *Client) do(req *http.Request, target any) error { slog.Error("failed to close response body", "error", err) } }(res.Body) + + body, err := io.ReadAll(res.Body) + if err != nil { + slog.Error("http response read failed", "method", req.Method, "path", req.URL.Path, "error", err) + return err + } if res.StatusCode != http.StatusOK { slog.Warn("http request returned non-200", "method", req.Method, "path", req.URL.Path, "status", res.Status) - return errors.New(res.Status) + return readBodyError(body, res.Status) } if target == nil { - var wrapper APIResponse[json.RawMessage] - if err = json.NewDecoder(res.Body).Decode(&wrapper); err != nil { - slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err) - return err - } - if !wrapper.Success { - slog.Warn("http api response failed", "method", req.Method, "path", req.URL.Path, "message", wrapper.Message) - return errors.New(wrapper.Message) - } return nil } - if err = json.NewDecoder(res.Body).Decode(target); err != nil { + if err = json.Unmarshal(body, target); err != nil { slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err) return err } return nil } + +func apiError(msg string) error { + if strings.TrimSpace(msg) == "" { + return nil + } + return errors.New(msg) +} + +func readBodyError(body []byte, fallback string) error { + var errBody struct { + ErrorMsg string `json:"error_msg"` + } + if err := json.Unmarshal(body, &errBody); err == nil && strings.TrimSpace(errBody.ErrorMsg) != "" { + return errors.New(errBody.ErrorMsg) + } + return errors.New(fallback) +} \ No newline at end of file