From 321b994d256f48ee3bf37d2e9f98bbf01928dcdb Mon Sep 17 00:00:00 2001 From: livepeer-tessa Date: Thu, 26 Mar 2026 18:25:52 +0000 Subject: [PATCH] fix: gracefully handle CURRENT_USER_HAS_NOT_PINNED_CID on IPFS unpin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the task-runner attempts to unpin an asset CID via Pinata and the CID was pinned under a different account, Pinata returns HTTP 400 with reason CURRENT_USER_HAS_NOT_PINNED_CID. Previously this caused the entire asset deletion to fail and the asset to remain stuck in the deleting queue. This change detects that specific error in the Unpin client method and treats it as a no-op (logs a warning and returns nil). The asset is not pinned by the current account so there is nothing to unpin — deletion should proceed normally. Adds three unit tests covering: - CURRENT_USER_HAS_NOT_PINNED_CID → no error returned - Other 400 errors → error still propagated - Successful 200 unpin → no error Fixes: daydreamlive/scope#747 Signed-off-by: livepeer-tessa --- clients/ipfs.go | 13 +++++++- clients/ipfs_test.go | 73 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 clients/ipfs_test.go diff --git a/clients/ipfs.go b/clients/ipfs.go index 166bd344..b2040269 100644 --- a/clients/ipfs.go +++ b/clients/ipfs.go @@ -4,8 +4,10 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" + "net/http" "strings" "time" @@ -101,10 +103,19 @@ func (p *pinataClient) PinContent(ctx context.Context, filename, fileContentType } func (p *pinataClient) Unpin(ctx context.Context, cid string) error { - return p.DoRequest(ctx, Request{ + err := p.DoRequest(ctx, Request{ Method: "DELETE", URL: "/pinning/unpin/" + cid, }, nil) + if err != nil { + var httpErr *HTTPStatusError + if errors.As(err, &httpErr) && httpErr.Status == http.StatusBadRequest && + strings.Contains(httpErr.Body, "CURRENT_USER_HAS_NOT_PINNED_CID") { + glog.Warningf("IPFS CID %s not pinned by current account, skipping unpin", cid) + return nil + } + } + return err } func (p *pinataClient) List(ctx context.Context, pageSize, pageOffset int) (pl *PinList, next int, err error) { diff --git a/clients/ipfs_test.go b/clients/ipfs_test.go new file mode 100644 index 00000000..2ba73caf --- /dev/null +++ b/clients/ipfs_test.go @@ -0,0 +1,73 @@ +package clients + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPinataUnpin_NotPinnedByCurrentUser(t *testing.T) { + // Simulate Pinata returning CURRENT_USER_HAS_NOT_PINNED_CID on DELETE + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error":{"reason":"CURRENT_USER_HAS_NOT_PINNED_CID","details":"The current user has not pinned the cid: bafkrei123","message":"The current user has not pinned the cid: bafkrei123"}}`)) + })) + defer srv.Close() + + client := &pinataClient{ + BaseClient: BaseClient{ + BaseUrl: srv.URL, + BaseHeaders: map[string]string{ + "Authorization": "Bearer test-jwt", + }, + }, + } + + err := client.Unpin(context.Background(), "bafkrei123") + require.NoError(t, err, "Unpin should not return an error when CID is not pinned by current user") +} + +func TestPinataUnpin_OtherError(t *testing.T) { + // Simulate a different 400 error (not CURRENT_USER_HAS_NOT_PINNED_CID) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error":{"reason":"SOME_OTHER_ERROR","message":"Something else went wrong"}}`)) + })) + defer srv.Close() + + client := &pinataClient{ + BaseClient: BaseClient{ + BaseUrl: srv.URL, + BaseHeaders: map[string]string{ + "Authorization": "Bearer test-jwt", + }, + }, + } + + err := client.Unpin(context.Background(), "bafkrei123") + require.Error(t, err, "Unpin should return an error for other HTTP 400 errors") +} + +func TestPinataUnpin_Success(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + client := &pinataClient{ + BaseClient: BaseClient{ + BaseUrl: srv.URL, + BaseHeaders: map[string]string{ + "Authorization": "Bearer test-jwt", + }, + }, + } + + err := client.Unpin(context.Background(), "bafkrei123") + require.NoError(t, err, "Unpin should succeed on 200") +}