From 5ac5f17d59a46cd06545afa9894c4cb372cbbb5d Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:44:51 -0700 Subject: [PATCH 1/2] Return 404 from force-leave for unknown nodes --- agent/agent_endpoint.go | 8 ++++++-- agent/agent_endpoint_test.go | 19 +++++++++++++++++++ agent/consul/client.go | 8 +++++++- agent/consul/server_ce.go | 2 +- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 0a44de54570..80b6bd74b0f 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -795,10 +795,14 @@ func (s *HTTPHandlers) AgentForceLeave(resp http.ResponseWriter, req *http.Reque addr := strings.TrimPrefix(req.URL.Path, "/v1/agent/force-leave/") if wan { - return nil, s.agent.ForceLeaveWAN(addr, prune, entMeta) + err = s.agent.ForceLeaveWAN(addr, prune, entMeta) } else { - return nil, s.agent.ForceLeave(addr, prune, entMeta) + err = s.agent.ForceLeave(addr, prune, entMeta) } + if errors.Is(err, consul.ErrNodeNotFound) { + return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: err.Error()} + } + return nil, err } // syncChanges is a helper function which wraps a blocking call to sync diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index a0bdf77b3d6..26446accaf5 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -2630,6 +2630,25 @@ func TestAgent_ForceLeave_ACLDeny(t *testing.T) { }) } +func TestAgent_ForceLeave_UnknownNode(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := NewTestAgent(t, "") + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + // Force-leaving a node that is not a cluster member is a client + // error, not a server error: the caller named something that does + // not exist, so the endpoint must answer 404 (see #21878). + req, _ := http.NewRequest("PUT", "/v1/agent/force-leave/nonexistent-node", nil) + resp := httptest.NewRecorder() + a.srv.h.ServeHTTP(resp, req) + require.Equal(t, http.StatusNotFound, resp.Code) +} + func TestAgent_ForceLeavePrune(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") diff --git a/agent/consul/client.go b/agent/consul/client.go index cecaf7b30ab..cd3d7f4a749 100644 --- a/agent/consul/client.go +++ b/agent/consul/client.go @@ -5,6 +5,7 @@ package consul import ( "context" + "errors" "fmt" "io" "strconv" @@ -258,6 +259,11 @@ func (c *Client) LANMembers(filter LANMemberFilter) ([]serf.Member, error) { return c.serf.Members(), nil } +// ErrNodeNotFound is returned when a node targeted by an operation, such +// as a force-leave, is not a member of the cluster. Callers that surface +// this to clients can map it to a 404 instead of a generic 500. +var ErrNodeNotFound = errors.New("agent: No node found") + // RemoveFailedNode is used to remove a failed node from the cluster. func (c *Client) RemoveFailedNode(node string, prune bool, entMeta *acl.EnterpriseMeta) error { // Partitions definitely have to match. @@ -266,7 +272,7 @@ func (c *Client) RemoveFailedNode(node string, prune bool, entMeta *acl.Enterpri c.config.AgentEnterpriseMeta().PartitionOrDefault(), entMeta.PartitionOrDefault()) } if !isSerfMember(c.serf, node) { - return fmt.Errorf("agent: No node found with name '%s'", node) + return fmt.Errorf("%w with name '%s'", ErrNodeNotFound, node) } if prune { return c.serf.RemoveFailedNodePrune(node) diff --git a/agent/consul/server_ce.go b/agent/consul/server_ce.go index 49c0be67bc9..2fc9937a3db 100644 --- a/agent/consul/server_ce.go +++ b/agent/consul/server_ce.go @@ -84,7 +84,7 @@ func (s *Server) removeFailedNode( } if !foundAny { - return fmt.Errorf("agent: No node found with name '%s'", node) + return fmt.Errorf("%w with name '%s'", ErrNodeNotFound, node) } return nil From cca6d1fa808a37d4df3f5843a922a2a6c365fe70 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:49:49 -0700 Subject: [PATCH 2/2] Add changelog entry for force-leave 404 fix --- .changelog/24082.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/24082.txt diff --git a/.changelog/24082.txt b/.changelog/24082.txt new file mode 100644 index 00000000000..f4d7bc6ee20 --- /dev/null +++ b/.changelog/24082.txt @@ -0,0 +1,3 @@ +```release-note:bug +http: return 404 instead of 500 when force-leaving a node that is not a cluster member +```