Repository: ObolNetwork/charon

Issue:
Catch HTTP 409 Duplicate Request gracefully in `charon dkg --publish`
# 🎯 Problem to be solved

When we complete a DKG and publish it. One node succeeds, the rest fail, and get an error that looks concerning. However its only the HTTP409 duplicate request because they're not the first node to publish. 

# 🛠️ Proposed solution

Lets just catch the 409 as expected and info log something like "Cluster lock is already published.". We should still throw any other 4XX or 5XX


# 🧪 Tests
- [ ] Tested by new automated unit/integration/smoke tests
- [ ] Manually tested on core team/canary/test clusters
- [ ] Manually tested on local compose simnet

<!-- Optional sections

# 👐 Additional acceptance criteria
List any additional acceptance criteria for this issue to be marked as closed.

# ❌ Out of Scope
If there is anything to highlight as out of scope for this issue, please outline it here.

-->


Discussion:

Solution patch:
diff --git a/.gitignore b/.gitignore
index b3bd4842..cc89875e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -159,3 +159,4 @@ changelog.md
 .claude/
 .charon**
 .anthropic/
+.serena/
diff --git a/app/obolapi/api.go b/app/obolapi/api.go
index ec684420..f297864d 100644
--- a/app/obolapi/api.go
+++ b/app/obolapi/api.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	"github.com/obolnetwork/charon/app/errors"
+	"github.com/obolnetwork/charon/app/log"
 	"github.com/obolnetwork/charon/app/z"
 	"github.com/obolnetwork/charon/cluster"
 )
@@ -106,7 +107,11 @@ func httpPost(ctx context.Context, url *url.URL, body []byte, headers map[string
 			return errors.Wrap(err, "read POST response", z.Int("status", res.StatusCode))
 		}
 
-		return errors.New("http POST failed", z.Int("status", res.StatusCode), z.Str("body", string(data)))
+		if res.StatusCode != http.StatusConflict {
+			return errors.New("http POST failed", z.Int("status", res.StatusCode), z.Str("body", string(data)))
+		}
+
+		log.Info(ctx, "Duplicate request ignored by Obol API. Likely another node submitted the same data already.", z.Int("status", res.StatusCode), z.Str("body", string(data)))
 	}
 
 	return nil



Test patch:
diff --git a/app/obolapi/api_internal_test.go b/app/obolapi/api_internal_test.go
index ecbd66c0..facb78f1 100644
--- a/app/obolapi/api_internal_test.go
+++ b/app/obolapi/api_internal_test.go
@@ -89,6 +89,22 @@ func TestHttpPost(t *testing.T) {
 			})),
 			expectedError: "POST failed",
 		},
+		{
+			name:     "status code 409 Conflict (duplicate ignored)",
+			body:     nil,
+			headers:  nil,
+			endpoint: "/post-request",
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				require.Equal(t, r.URL.Path, "/post-request")
+				require.Equal(t, r.Method, http.MethodPost)
+				require.Equal(t, r.Header.Get("Content-Type"), "application/json")
+
+				w.WriteHeader(http.StatusConflict)
+				_, err := w.Write([]byte(`"Duplicate request"`))
+				require.NoError(t, err)
+			})),
+			expectedError: "",
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {