From 67efc3bbd3bed901751a5d0ca7b4d3fc08cc85e7 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:56:37 -0600 Subject: [PATCH 01/15] feat: make both feed versions race together --- pkg/api/bzz.go | 75 ++++++++++++++++++++++++++++++++++++++++++--- pkg/api/bzz_test.go | 6 ++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index a27e3f813c5..9648ec3f196 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -380,6 +380,73 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) { s.serveReference(logger, address, paths.Path, w, r, true, queries.FeedLegacyResolve) } +type getWrappedResult struct { + ch swarm.Chunk + err error +} + +// resolveFeed races the resolution of both types of feeds. it returns the first correct feed found or an error. +func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swarm.Chunk) (swarm.Chunk, error) { + innerCtx, cancel := context.WithCancel(ctx) + getWrapped := func(v1 bool) chan getWrappedResult { + ret := make(chan getWrappedResult) + go func() { + wc, err := feeds.GetWrappedChunk(innerCtx, getter, ch, v1) + if err != nil { + select { + case ret <- getWrappedResult{nil, err}: + return + case <-innerCtx.Done(): + return + } + } + + // here we just check whether the address is retrievable. + // if it returns an error we send that over the channel, otherwise + // we send the wc chunk back to the caller so that the feed can be + // dereferenced. + _, err = getter.Get(innerCtx, wc.Address()) + if err != nil { + select { + case ret <- getWrappedResult{nil, err}: + return + case <-innerCtx.Done(): + return + } + } + select { + case ret <- getWrappedResult{wc, nil}: + return + case <-innerCtx.Done(): + return + } + }() + return ret + } + + v1 := getWrapped(true) + v2 := getWrapped(false) + + select { + case v1r := <-v1: + if v1r.ch != nil { + cancel() + return v1r.ch, nil + } + // wait for the other one + v2r := <-v2 + return v2r.ch, v2r.err + case v2r := <-v2: + if v2r.ch != nil { + cancel() + return v2r.ch, nil + } + // wait for the other one + v1r := <-v1 + return v1r.ch, v1r.err + } +} + func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathVar string, w http.ResponseWriter, r *http.Request, headerOnly bool, feedLegacyResolve bool) { loggerV1 := logger.V(1).Build() @@ -415,7 +482,6 @@ func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathV jsonhttp.BadRequest(w, "could not parse headers") return } - FETCH: // read manifest entry m, err := manifest.NewDefaultManifestReference( @@ -449,7 +515,8 @@ FETCH: jsonhttp.NotFound(w, "no update found") return } - wc, err := feeds.GetWrappedChunk(ctx, s.storer.Download(cache), ch, feedLegacyResolve) + + wc, err := s.resolveFeed(ctx, s.storer.Download(cache), ch) if err != nil { if errors.Is(err, feeds.ErrNotLegacyPayload) { logger.Debug("bzz: download: feed is not a legacy payload") @@ -468,10 +535,10 @@ FETCH: jsonhttp.InternalServerError(w, "mapStructure feed update") return } + address = wc.Address() // modify ls and init with non-existing wrapped chunk ls = loadsave.NewReadonlyWithRootCh(s.storer.Download(cache), s.storer.Cache(), wc, rLevel) - feedDereferenced = true curBytes, err := cur.MarshalBinary() if err != nil { @@ -490,7 +557,6 @@ FETCH: goto FETCH } } - if pathVar == "" { loggerV1.Debug("bzz download: handle empty path", "address", address) @@ -505,6 +571,7 @@ FETCH: return } } + logger.Debug("bzz download: address not found or incorrect", "address", address, "path", pathVar) logger.Error(nil, "address not found or incorrect") jsonhttp.NotFound(w, "address not found or incorrect") diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index 7f636476d1f..a12a4c17bf4 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -846,7 +846,7 @@ func TestFeedIndirection(t *testing.T) { Feeds: factory, }) - jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", true), http.StatusOK, + jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusOK, jsonhttptest.WithExpectedResponse(updateData), jsonhttptest.WithExpectedContentLength(len(updateData)), jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), @@ -855,7 +855,7 @@ func TestFeedIndirection(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), ) - jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusNotFound) + // jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusNotFound) }) t.Run("wrapped feed", func(t *testing.T) { @@ -885,7 +885,7 @@ func TestFeedIndirection(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), ) - jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", true), http.StatusBadRequest) + // jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", true), http.StatusBadRequest) }) } From 4a655771ef5dbad49ead0ce9d7caddd3c36ab30e Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:36:31 -0600 Subject: [PATCH 02/15] chore: lint --- pkg/api/bzz.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index 9648ec3f196..fde4ae64fec 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -388,6 +388,7 @@ type getWrappedResult struct { // resolveFeed races the resolution of both types of feeds. it returns the first correct feed found or an error. func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swarm.Chunk) (swarm.Chunk, error) { innerCtx, cancel := context.WithCancel(ctx) + defer cancel() getWrapped := func(v1 bool) chan getWrappedResult { ret := make(chan getWrappedResult) go func() { From e3cb3a3fa27d4d0081f24c1263dc7b2c100132d5 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:29:52 -0600 Subject: [PATCH 03/15] chore: fetch both only on v1 ambiguity --- pkg/api/bzz.go | 57 +++++++++++++++++++++++++++++++-------------- pkg/api/bzz_test.go | 1 - pkg/feeds/getter.go | 15 ++++++++++-- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index fde4ae64fec..86863b9f03f 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -424,27 +424,50 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa }() return ret } + isV1, err := feeds.IsV1Payload(ch) + if err != nil { + return nil, err + } + // if we have v1 length, it means there's ambiguity so we + // should fetch both feed versions. if the length isn't v1 + // then we should only try to fetch v2. + var ( + v1, v2 chan getWrappedResult + both = false + ) + if isV1 { + both = true + v1 = getWrapped(true) + v2 = getWrapped(false) + } else { + v2 = getWrapped(false) + } - v1 := getWrapped(true) - v2 := getWrapped(false) - - select { - case v1r := <-v1: - if v1r.ch != nil { - cancel() - return v1r.ch, nil + processChanOutput := func(result getWrappedResult, other chan getWrappedResult) (swarm.Chunk, error) { + defer cancel() + if !both { + return result.ch, result.err } - // wait for the other one - v2r := <-v2 - return v2r.ch, v2r.err - case v2r := <-v2: - if v2r.ch != nil { - cancel() - return v2r.ch, nil + // both are being checked. if there's no err return the chunk + // otherwise wait for the other channel + if result.err == nil { + return result.ch, nil } // wait for the other one - v1r := <-v1 - return v1r.ch, v1r.err + select { + case result := <-other: + return result.ch, result.err + case <-innerCtx.Done(): + return nil, ctx.Err() + } + } + select { + case v1r := <-v1: + return processChanOutput(v1r, v2) + case v2r := <-v2: + return processChanOutput(v2r, v1) + case <-innerCtx.Done(): + return nil, ctx.Err() } } diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index a12a4c17bf4..8e98ac3337f 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -835,7 +835,6 @@ func TestFeedIndirection(t *testing.T) { t.Run("legacy feed", func(t *testing.T) { feedUpdate := toChunk(t, 121212, resp.Reference.Bytes()) - var ( look = newMockLookup(-1, 0, feedUpdate, nil, &id{}, nil) factory = newMockFactory(look) diff --git a/pkg/feeds/getter.go b/pkg/feeds/getter.go index 2bdf509e282..40ccf98bca7 100644 --- a/pkg/feeds/getter.go +++ b/pkg/feeds/getter.go @@ -93,9 +93,20 @@ func FromChunk(ch swarm.Chunk) (swarm.Chunk, error) { // legacyPayload returns back the referenced chunk and datetime from the legacy feed payload func legacyPayload(wrappedChunk swarm.Chunk) (swarm.Address, error) { cacData := wrappedChunk.Data() - if len(cacData) != 16+swarm.HashSize && len(cacData) != 16+swarm.HashSize*2 { + if !isV1Length(len(cacData)) { return swarm.ZeroAddress, ErrNotLegacyPayload } - return swarm.NewAddress(cacData[16:]), nil } + +func IsV1Payload(ch swarm.Chunk) (bool, error) { + cc, err := FromChunk(ch) + if err != nil { + return false, err + } + return isV1Length(len(cc.Data())), nil +} + +func isV1Length(length int) bool { + return length == 16+swarm.HashSize || length == 16+swarm.HashSize*2 +} From 166edba0d462b8b06e498bd3573df3deb8a83ebe Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:14:39 -0600 Subject: [PATCH 04/15] chore: remove the resolve param --- pkg/api/bzz.go | 22 +++------------------- pkg/api/subdomain.go | 10 +--------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index 86863b9f03f..bd0d4e9c919 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -337,15 +337,7 @@ func (s *Service) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some. } - queries := struct { - FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"` - }{} - if response := s.mapStructure(r.URL.Query(), &queries); response != nil { - response("invalid query params", logger, w) - return - } - - s.serveReference(logger, address, paths.Path, w, r, false, queries.FeedLegacyResolve) + s.serveReference(logger, address, paths.Path, w, r, false) } func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) { @@ -360,14 +352,6 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) { return } - queries := struct { - FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"` - }{} - if response := s.mapStructure(r.URL.Query(), &queries); response != nil { - response("invalid query params", logger, w) - return - } - address := paths.Address if v := getAddressFromContext(r.Context()); !v.IsZero() { address = v @@ -377,7 +361,7 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) { paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some. } - s.serveReference(logger, address, paths.Path, w, r, true, queries.FeedLegacyResolve) + s.serveReference(logger, address, paths.Path, w, r, true) } type getWrappedResult struct { @@ -471,7 +455,7 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa } } -func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathVar string, w http.ResponseWriter, r *http.Request, headerOnly bool, feedLegacyResolve bool) { +func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathVar string, w http.ResponseWriter, r *http.Request, headerOnly bool) { loggerV1 := logger.V(1).Build() headers := struct { diff --git a/pkg/api/subdomain.go b/pkg/api/subdomain.go index b30e270a4ec..5d5cede467a 100644 --- a/pkg/api/subdomain.go +++ b/pkg/api/subdomain.go @@ -28,13 +28,5 @@ func (s *Service) subdomainHandler(w http.ResponseWriter, r *http.Request) { paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some. } - queries := struct { - FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"` - }{} - if response := s.mapStructure(r.URL.Query(), &queries); response != nil { - response("invalid query params", logger, w) - return - } - - s.serveReference(logger, paths.Subdomain, paths.Path, w, r, false, queries.FeedLegacyResolve) + s.serveReference(logger, paths.Subdomain, paths.Path, w, r, false) } From feed2b48aa997999be68309eedb6eb58c8d2449e Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:41:24 -0600 Subject: [PATCH 05/15] chore: still one test to go --- pkg/api/api.go | 1 + pkg/api/bzz.go | 51 +++++++++++++++++++++++++++++++++----------- pkg/api/feed.go | 21 +++++++++--------- pkg/api/feed_test.go | 40 +++++++++++++++++----------------- 4 files changed, 71 insertions(+), 42 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 5f747237bc4..3ab9a0eabb4 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -79,6 +79,7 @@ const ( SwarmSocSignatureHeader = "Swarm-Soc-Signature" SwarmFeedIndexHeader = "Swarm-Feed-Index" SwarmFeedIndexNextHeader = "Swarm-Feed-Index-Next" + SwarmFeedResolvedVersionHeader = "Swarm-Feed-Resolved-Version" SwarmOnlyRootChunk = "Swarm-Only-Root-Chunk" SwarmCollectionHeader = "Swarm-Collection" SwarmPostageBatchIdHeader = "Swarm-Postage-Batch-Id" diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index bd0d4e9c919..fce6d49d893 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -369,8 +369,11 @@ type getWrappedResult struct { err error } -// resolveFeed races the resolution of both types of feeds. it returns the first correct feed found or an error. -func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swarm.Chunk) (swarm.Chunk, error) { +// resolveFeed races the resolution of both types of feeds. +// the resolveInner controls whether we really try to dereference the inner resource or just +// figure out if its a v1 or v2 chunk. +// it returns the first correct feed found, the type found ("v1" or "v2") or an error. +func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swarm.Chunk, resolveInner bool) (swarm.Chunk, string, error) { innerCtx, cancel := context.WithCancel(ctx) defer cancel() getWrapped := func(v1 bool) chan getWrappedResult { @@ -385,7 +388,15 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa return } } - + fmt.Printf("b4 resolve inner %v %v %v\n", v1, wc, err) + if !resolveInner { + select { + case ret <- getWrappedResult{wc, nil}: + return + case <-innerCtx.Done(): + return + } + } // here we just check whether the address is retrievable. // if it returns an error we send that over the channel, otherwise // we send the wc chunk back to the caller so that the feed can be @@ -410,7 +421,7 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa } isV1, err := feeds.IsV1Payload(ch) if err != nil { - return nil, err + return nil, "", err } // if we have v1 length, it means there's ambiguity so we // should fetch both feed versions. if the length isn't v1 @@ -420,38 +431,52 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa both = false ) if isV1 { + fmt.Println("both") both = true v1 = getWrapped(true) v2 = getWrapped(false) } else { + + fmt.Println("nboth") v2 = getWrapped(false) } - processChanOutput := func(result getWrappedResult, other chan getWrappedResult) (swarm.Chunk, error) { + // closure to handle processing one channel then the other. + // the "resolving" parameter is meant to tell the closure which feed type is in the result struct + // which in turns allows it to return which feed type was resolved. + processChanOutput := func(resolving string, result getWrappedResult, other chan getWrappedResult) (swarm.Chunk, string, error) { defer cancel() if !both { - return result.ch, result.err + return result.ch, resolving, result.err } + fmt.Println("resolving both") // both are being checked. if there's no err return the chunk // otherwise wait for the other channel if result.err == nil { - return result.ch, nil + fmt.Println("resolving both2") + return result.ch, resolving, nil + } + if resolving == "v1" { + resolving = "v2" + } else { + resolving = "v1" } // wait for the other one select { case result := <-other: - return result.ch, result.err + fmt.Println("resolving both3") + return result.ch, resolving, result.err case <-innerCtx.Done(): - return nil, ctx.Err() + return nil, "", ctx.Err() } } select { case v1r := <-v1: - return processChanOutput(v1r, v2) + return processChanOutput("v1", v1r, v2) case v2r := <-v2: - return processChanOutput(v2r, v1) + return processChanOutput("v2", v2r, v1) case <-innerCtx.Done(): - return nil, ctx.Err() + return nil, "", ctx.Err() } } @@ -524,7 +549,7 @@ FETCH: return } - wc, err := s.resolveFeed(ctx, s.storer.Download(cache), ch) + wc, _, err := s.resolveFeed(ctx, s.storer.Download(cache), ch, true) if err != nil { if errors.Is(err, feeds.ErrNotLegacyPayload) { logger.Debug("bzz: download: feed is not a legacy payload") diff --git a/pkg/api/feed.go b/pkg/api/feed.go index 313b04c831a..78b6c42c719 100644 --- a/pkg/api/feed.go +++ b/pkg/api/feed.go @@ -8,6 +8,7 @@ import ( "bytes" "encoding/hex" "errors" + "fmt" "io" "net/http" "strconv" @@ -53,9 +54,8 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) { } queries := struct { - At int64 `map:"at"` - After uint64 `map:"after"` - FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"` + At int64 `map:"at"` + After uint64 `map:"after"` }{} if response := s.mapStructure(r.URL.Query(), &queries); response != nil { response("invalid query params", logger, w) @@ -72,7 +72,6 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) { response("invalid header params", logger, w) return } - f := feeds.New(paths.Topic, paths.Owner) lookup, err := s.feedFactory.NewLookup(feeds.Sequence, f) if err != nil { @@ -103,7 +102,8 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) { return } - wc, err := feeds.GetWrappedChunk(r.Context(), s.storer.Download(false), ch, queries.FeedLegacyResolve) + wc, feedVer, err := s.resolveFeed(r.Context(), s.storer.Download(false), ch, true) + fmt.Println(feedVer) if err != nil { logger.Error(nil, "wrapped chunk cannot be retrieved") jsonhttp.NotFound(w, "wrapped chunk cannot be retrieved") @@ -135,11 +135,12 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) { sig := socCh.Signature() additionalHeaders := http.Header{ - ContentTypeHeader: {"application/octet-stream"}, - SwarmFeedIndexHeader: {hex.EncodeToString(curBytes)}, - SwarmFeedIndexNextHeader: {hex.EncodeToString(nextBytes)}, - SwarmSocSignatureHeader: {hex.EncodeToString(sig)}, - AccessControlExposeHeaders: {SwarmFeedIndexHeader, SwarmFeedIndexNextHeader, SwarmSocSignatureHeader}, + ContentTypeHeader: {"application/octet-stream"}, + SwarmFeedIndexHeader: {hex.EncodeToString(curBytes)}, + SwarmFeedIndexNextHeader: {hex.EncodeToString(nextBytes)}, + SwarmSocSignatureHeader: {hex.EncodeToString(sig)}, + SwarmFeedResolvedVersionHeader: {feedVer}, + AccessControlExposeHeaders: {SwarmFeedIndexHeader, SwarmFeedIndexNextHeader, SwarmSocSignatureHeader}, } if headers.OnlyRootChunk { diff --git a/pkg/api/feed_test.go b/pkg/api/feed_test.go index fcfe4b93e68..5faab3ac7e8 100644 --- a/pkg/api/feed_test.go +++ b/pkg/api/feed_test.go @@ -15,7 +15,6 @@ import ( "math/big" "net/http" "net/url" - "strconv" "testing" "github.com/ethersphere/bee/v2/pkg/api" @@ -41,17 +40,14 @@ const ownerString = "8d3766440f0d7b949a5e32995d09619a7f86e632" var expReference = swarm.MustParseHexAddress("891a1d1c8436c792d02fc2e8883fef7ab387eaeaacd25aa9f518be7be7856d54") func TestFeed_Get(t *testing.T) { - t.Parallel() + // t.Parallel() var ( - feedResource = func(owner, topic, at string, legacyFeed bool) string { + feedResource = func(owner, topic, at string) string { values := url.Values{} if at != "" { values.Set("at", at) } - if legacyFeed { - values.Set("swarm-feed-legacy-resolve", strconv.FormatBool(legacyFeed)) - } baseURL := fmt.Sprintf("/feeds/%s/%s", owner, topic) if len(values) > 0 { @@ -72,7 +68,8 @@ func TestFeed_Get(t *testing.T) { } t.Run("with at", func(t *testing.T) { - t.Parallel() + // t.Skip() + // t.Parallel() var ( timestamp = int64(12121212) @@ -86,7 +83,7 @@ func TestFeed_Get(t *testing.T) { }) ) - jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "12", true), http.StatusOK, + jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "12"), http.StatusOK, jsonhttptest.WithExpectedResponse(mockWrappedCh.Data()[swarm.SpanSize:]), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)), jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), @@ -98,7 +95,8 @@ func TestFeed_Get(t *testing.T) { }) t.Run("latest with legacy payload", func(t *testing.T) { - t.Parallel() + // t.Skip() + // t.Parallel() var ( timestamp = int64(12121212) @@ -113,7 +111,7 @@ func TestFeed_Get(t *testing.T) { }) ) - jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", true), http.StatusOK, + jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK, jsonhttptest.WithExpectedResponse(mockWrappedCh.Data()[swarm.SpanSize:]), jsonhttptest.WithExpectedContentLength(len(mockWrappedCh.Data()[swarm.SpanSize:])), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)), @@ -126,7 +124,8 @@ func TestFeed_Get(t *testing.T) { }) t.Run("chunk wrapping", func(t *testing.T) { - t.Parallel() + // t.Skip() + // t.Parallel() testData := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8} @@ -142,7 +141,7 @@ func TestFeed_Get(t *testing.T) { }) ) - jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusOK, + jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK, jsonhttptest.WithExpectedResponse(testData), jsonhttptest.WithExpectedContentLength(len(testData)), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)), @@ -155,7 +154,8 @@ func TestFeed_Get(t *testing.T) { }) t.Run("legacy payload with non existing wrapped chunk", func(t *testing.T) { - t.Parallel() + // t.Skip() + // t.Parallel() wrappedRef := make([]byte, swarm.HashSize) _ = copy(wrappedRef, mockWrappedCh.Address().Bytes()) @@ -172,11 +172,12 @@ func TestFeed_Get(t *testing.T) { }) ) - jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", true), http.StatusNotFound) + jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusNotFound) }) t.Run("query parameter legacy feed resolve", func(t *testing.T) { - t.Parallel() + // t.Skip() + // t.Parallel() var ( look = newMockLookup(1, 0, nil, errors.New("dummy"), &id{}, &id{}) @@ -188,11 +189,12 @@ func TestFeed_Get(t *testing.T) { ) // Test with the legacyFeed parameter set to true which should add the query parameter - jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusNotFound) + jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusNotFound) }) t.Run("bigger payload than one chunk", func(t *testing.T) { - t.Parallel() + // t.Skip() + // t.Parallel() testDataLen := 5000 testData := testutil.RandBytesWithSeed(t, testDataLen, 1) @@ -220,7 +222,7 @@ func TestFeed_Get(t *testing.T) { ) t.Run("retrieve chunk tree", func(t *testing.T) { - jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusOK, + jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK, jsonhttptest.WithExpectedResponse(testData), jsonhttptest.WithExpectedContentLength(testDataLen), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)), @@ -233,7 +235,7 @@ func TestFeed_Get(t *testing.T) { }) t.Run("retrieve only wrapped chunk", func(t *testing.T) { - jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusOK, + jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK, jsonhttptest.WithRequestHeader(api.SwarmOnlyRootChunk, "true"), jsonhttptest.WithExpectedResponse(testRootCh.Data()), jsonhttptest.WithExpectedContentLength(len(testRootCh.Data())), From 5a4d6cdafb3c53c8bc6a2628841859626b2e814f Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:48:33 -0600 Subject: [PATCH 06/15] chore: fix tests --- pkg/api/bzz.go | 36 ++++++++++++++++++------------------ pkg/api/feed.go | 3 --- pkg/api/feed_test.go | 22 +++++++++------------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index fce6d49d893..a885d04b9d5 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -366,6 +366,7 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) { type getWrappedResult struct { ch swarm.Chunk + ver string err error } @@ -377,26 +378,24 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa innerCtx, cancel := context.WithCancel(ctx) defer cancel() getWrapped := func(v1 bool) chan getWrappedResult { + ver := "v1" + if !v1 { + ver = "v2" + } ret := make(chan getWrappedResult) go func() { wc, err := feeds.GetWrappedChunk(innerCtx, getter, ch, v1) if err != nil { select { - case ret <- getWrappedResult{nil, err}: - return - case <-innerCtx.Done(): - return - } - } - fmt.Printf("b4 resolve inner %v %v %v\n", v1, wc, err) - if !resolveInner { - select { - case ret <- getWrappedResult{wc, nil}: + case ret <- getWrappedResult{nil, ver, err}: return case <-innerCtx.Done(): return } } + + // v2 might be data verbatim, which makes trying to resolve it useless + // here we just check whether the address is retrievable. // if it returns an error we send that over the channel, otherwise // we send the wc chunk back to the caller so that the feed can be @@ -404,14 +403,14 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa _, err = getter.Get(innerCtx, wc.Address()) if err != nil { select { - case ret <- getWrappedResult{nil, err}: + case ret <- getWrappedResult{wc, ver, err}: return case <-innerCtx.Done(): return } } select { - case ret <- getWrappedResult{wc, nil}: + case ret <- getWrappedResult{wc, ver, nil}: return case <-innerCtx.Done(): return @@ -430,14 +429,12 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa v1, v2 chan getWrappedResult both = false ) + // isV1 = true if isV1 { - fmt.Println("both") both = true v1 = getWrapped(true) v2 = getWrapped(false) } else { - - fmt.Println("nboth") v2 = getWrapped(false) } @@ -447,13 +444,14 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa processChanOutput := func(resolving string, result getWrappedResult, other chan getWrappedResult) (swarm.Chunk, string, error) { defer cancel() if !both { + if resolving == "v2" { + return result.ch, resolving, nil + } return result.ch, resolving, result.err } - fmt.Println("resolving both") // both are being checked. if there's no err return the chunk // otherwise wait for the other channel if result.err == nil { - fmt.Println("resolving both2") return result.ch, resolving, nil } if resolving == "v1" { @@ -464,7 +462,9 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa // wait for the other one select { case result := <-other: - fmt.Println("resolving both3") + if result.ver == "v2" { + return result.ch, resolving, nil + } return result.ch, resolving, result.err case <-innerCtx.Done(): return nil, "", ctx.Err() diff --git a/pkg/api/feed.go b/pkg/api/feed.go index 78b6c42c719..7e0ef26ef47 100644 --- a/pkg/api/feed.go +++ b/pkg/api/feed.go @@ -8,7 +8,6 @@ import ( "bytes" "encoding/hex" "errors" - "fmt" "io" "net/http" "strconv" @@ -85,7 +84,6 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) { } return } - ch, cur, next, err := lookup.At(r.Context(), queries.At, queries.After) if err != nil { logger.Debug("lookup at failed", "at", queries.At, "error", err) @@ -103,7 +101,6 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) { } wc, feedVer, err := s.resolveFeed(r.Context(), s.storer.Download(false), ch, true) - fmt.Println(feedVer) if err != nil { logger.Error(nil, "wrapped chunk cannot be retrieved") jsonhttp.NotFound(w, "wrapped chunk cannot be retrieved") diff --git a/pkg/api/feed_test.go b/pkg/api/feed_test.go index 5faab3ac7e8..fb685190970 100644 --- a/pkg/api/feed_test.go +++ b/pkg/api/feed_test.go @@ -40,7 +40,7 @@ const ownerString = "8d3766440f0d7b949a5e32995d09619a7f86e632" var expReference = swarm.MustParseHexAddress("891a1d1c8436c792d02fc2e8883fef7ab387eaeaacd25aa9f518be7be7856d54") func TestFeed_Get(t *testing.T) { - // t.Parallel() + t.Parallel() var ( feedResource = func(owner, topic, at string) string { @@ -68,8 +68,7 @@ func TestFeed_Get(t *testing.T) { } t.Run("with at", func(t *testing.T) { - // t.Skip() - // t.Parallel() + t.Parallel() var ( timestamp = int64(12121212) @@ -95,8 +94,7 @@ func TestFeed_Get(t *testing.T) { }) t.Run("latest with legacy payload", func(t *testing.T) { - // t.Skip() - // t.Parallel() + t.Parallel() var ( timestamp = int64(12121212) @@ -124,8 +122,7 @@ func TestFeed_Get(t *testing.T) { }) t.Run("chunk wrapping", func(t *testing.T) { - // t.Skip() - // t.Parallel() + t.Parallel() testData := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8} @@ -154,8 +151,7 @@ func TestFeed_Get(t *testing.T) { }) t.Run("legacy payload with non existing wrapped chunk", func(t *testing.T) { - // t.Skip() - // t.Parallel() + t.Skip() wrappedRef := make([]byte, swarm.HashSize) _ = copy(wrappedRef, mockWrappedCh.Address().Bytes()) @@ -176,8 +172,7 @@ func TestFeed_Get(t *testing.T) { }) t.Run("query parameter legacy feed resolve", func(t *testing.T) { - // t.Skip() - // t.Parallel() + t.Parallel() var ( look = newMockLookup(1, 0, nil, errors.New("dummy"), &id{}, &id{}) @@ -193,8 +188,7 @@ func TestFeed_Get(t *testing.T) { }) t.Run("bigger payload than one chunk", func(t *testing.T) { - // t.Skip() - // t.Parallel() + t.Parallel() testDataLen := 5000 testData := testutil.RandBytesWithSeed(t, testDataLen, 1) @@ -386,9 +380,11 @@ func (l *mockLookup) At(_ context.Context, at int64, after uint64) (swarm.Chunk, // shortcut to ignore the value in the call since time.Now() is a moving target return l.chunk, l.cur, l.next, nil } + if at == l.at && after == l.after { return l.chunk, l.cur, l.next, nil } + return nil, nil, nil, errors.New("no feed update found") } From bca89ea603a240599a9dc086d3c82c3932bf5348 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:10:03 -0600 Subject: [PATCH 07/15] chore: fix tests and code cleanups --- pkg/api/bzz.go | 18 ++++++++---------- pkg/api/bzz_test.go | 2 ++ pkg/api/feed_test.go | 11 +++++++++++ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index a885d04b9d5..fc43d0135d0 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -366,7 +366,7 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) { type getWrappedResult struct { ch swarm.Chunk - ver string + v1 bool // indicates whether the feed that was resolved is v1. false if v2 err error } @@ -378,16 +378,12 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa innerCtx, cancel := context.WithCancel(ctx) defer cancel() getWrapped := func(v1 bool) chan getWrappedResult { - ver := "v1" - if !v1 { - ver = "v2" - } ret := make(chan getWrappedResult) go func() { wc, err := feeds.GetWrappedChunk(innerCtx, getter, ch, v1) if err != nil { select { - case ret <- getWrappedResult{nil, ver, err}: + case ret <- getWrappedResult{nil, v1, err}: return case <-innerCtx.Done(): return @@ -403,14 +399,14 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa _, err = getter.Get(innerCtx, wc.Address()) if err != nil { select { - case ret <- getWrappedResult{wc, ver, err}: + case ret <- getWrappedResult{wc, v1, err}: return case <-innerCtx.Done(): return } } select { - case ret <- getWrappedResult{wc, ver, nil}: + case ret <- getWrappedResult{wc, v1, nil}: return case <-innerCtx.Done(): return @@ -462,7 +458,8 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa // wait for the other one select { case result := <-other: - if result.ver == "v2" { + if !result.v1 { + // resolving v2 return result.ch, resolving, nil } return result.ch, resolving, result.err @@ -549,7 +546,7 @@ FETCH: return } - wc, _, err := s.resolveFeed(ctx, s.storer.Download(cache), ch, true) + wc, feedVer, err := s.resolveFeed(ctx, s.storer.Download(cache), ch, true) if err != nil { if errors.Is(err, feeds.ErrNotLegacyPayload) { logger.Debug("bzz: download: feed is not a legacy payload") @@ -582,6 +579,7 @@ FETCH: } w.Header().Set(SwarmFeedIndexHeader, hex.EncodeToString(curBytes)) + w.Header().Set(SwarmFeedResolvedVersionHeader, feedVer) // this header might be overriding others. handle with care. in the future // we should implement an append functionality for this specific header, // since different parts of handlers might be overriding others' values diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index 8e98ac3337f..a9bef9c1d31 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -852,6 +852,7 @@ func TestFeedIndirection(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, `inline; filename="index.html"`), jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedResolvedVersionHeader, "v1"), ) // jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusNotFound) @@ -882,6 +883,7 @@ func TestFeedIndirection(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, `inline; filename="index.html"`), jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedResolvedVersionHeader, "v2"), ) // jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", true), http.StatusBadRequest) diff --git a/pkg/api/feed_test.go b/pkg/api/feed_test.go index fb685190970..e33eba63d0c 100644 --- a/pkg/api/feed_test.go +++ b/pkg/api/feed_test.go @@ -90,6 +90,7 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), + jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedResolvedVersionHeader, "v1"), ) }) @@ -118,6 +119,7 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), + jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedResolvedVersionHeader, "v1"), ) }) @@ -147,11 +149,18 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), + jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedResolvedVersionHeader, "v2"), ) }) t.Run("legacy payload with non existing wrapped chunk", func(t *testing.T) { t.Skip() + /* + This test has been disabled since it cannot be supported with the automatic + Feed resolution logic that is now in place. In case automatic feed resolution + would be removed at some point, this test can be reactived. The issue is + thoroughly described in the PR: https://github.com/ethersphere/bee/pull/5287 + */ wrappedRef := make([]byte, swarm.HashSize) _ = copy(wrappedRef, mockWrappedCh.Address().Bytes()) @@ -225,6 +234,7 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), + jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedResolvedVersionHeader, "v2"), ) }) @@ -238,6 +248,7 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexNextHeader), jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), + jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedResolvedVersionHeader, "v2"), ) }) }) From 0abde881defbf096b02e92a38b1de48a5d0cae70 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:21:23 -0600 Subject: [PATCH 08/15] chore: remove commented tests --- pkg/api/bzz_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index a9bef9c1d31..cc3035b234e 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -854,8 +854,6 @@ func TestFeedIndirection(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedResolvedVersionHeader, "v1"), ) - - // jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusNotFound) }) t.Run("wrapped feed", func(t *testing.T) { @@ -885,8 +883,6 @@ func TestFeedIndirection(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedResolvedVersionHeader, "v2"), ) - - // jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", true), http.StatusBadRequest) }) } From 32370e62118526c192e6dbec9139be5d4cbadd81 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:44:39 -0600 Subject: [PATCH 09/15] chore: more cleanups --- pkg/api/bzz.go | 8 ++------ pkg/api/bzz_test.go | 10 +++------- pkg/api/feed.go | 2 +- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index fc43d0135d0..2e16a607910 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -371,10 +371,9 @@ type getWrappedResult struct { } // resolveFeed races the resolution of both types of feeds. -// the resolveInner controls whether we really try to dereference the inner resource or just // figure out if its a v1 or v2 chunk. // it returns the first correct feed found, the type found ("v1" or "v2") or an error. -func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swarm.Chunk, resolveInner bool) (swarm.Chunk, string, error) { +func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swarm.Chunk) (swarm.Chunk, string, error) { innerCtx, cancel := context.WithCancel(ctx) defer cancel() getWrapped := func(v1 bool) chan getWrappedResult { @@ -390,8 +389,6 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa } } - // v2 might be data verbatim, which makes trying to resolve it useless - // here we just check whether the address is retrievable. // if it returns an error we send that over the channel, otherwise // we send the wc chunk back to the caller so that the feed can be @@ -425,7 +422,6 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa v1, v2 chan getWrappedResult both = false ) - // isV1 = true if isV1 { both = true v1 = getWrapped(true) @@ -546,7 +542,7 @@ FETCH: return } - wc, feedVer, err := s.resolveFeed(ctx, s.storer.Download(cache), ch, true) + wc, feedVer, err := s.resolveFeed(ctx, s.storer.Download(cache), ch) if err != nil { if errors.Is(err, feeds.ErrNotLegacyPayload) { logger.Debug("bzz: download: feed is not a legacy payload") diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index cc3035b234e..c12c108d418 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -764,12 +764,8 @@ func TestFeedIndirection(t *testing.T) { Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), }) - bzzDownloadResource = func(addr, path string, legacyFeed bool) string { + bzzDownloadResource = func(addr, path string) string { values := url.Values{} - if legacyFeed { - values.Set("swarm-feed-legacy-resolve", strconv.FormatBool(legacyFeed)) - } - baseURL := "/bzz/" + addr + "/" + path if len(values) > 0 { return baseURL + "?" + values.Encode() @@ -845,7 +841,7 @@ func TestFeedIndirection(t *testing.T) { Feeds: factory, }) - jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusOK, + jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), ""), http.StatusOK, jsonhttptest.WithExpectedResponse(updateData), jsonhttptest.WithExpectedContentLength(len(updateData)), jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), @@ -874,7 +870,7 @@ func TestFeedIndirection(t *testing.T) { Feeds: factory, }) - jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusOK, + jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), ""), http.StatusOK, jsonhttptest.WithExpectedResponse(updateData), jsonhttptest.WithExpectedContentLength(len(updateData)), jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), diff --git a/pkg/api/feed.go b/pkg/api/feed.go index 7e0ef26ef47..cc73fc6f3df 100644 --- a/pkg/api/feed.go +++ b/pkg/api/feed.go @@ -100,7 +100,7 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) { return } - wc, feedVer, err := s.resolveFeed(r.Context(), s.storer.Download(false), ch, true) + wc, feedVer, err := s.resolveFeed(r.Context(), s.storer.Download(false), ch) if err != nil { logger.Error(nil, "wrapped chunk cannot be retrieved") jsonhttp.NotFound(w, "wrapped chunk cannot be retrieved") From 909173cfd6ff1b0ba3a259580d9943abf668a002 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:48:25 -0600 Subject: [PATCH 10/15] docs: fix openapi spec --- openapi/Swarm.yaml | 14 +++++++++++--- openapi/SwarmCommon.yaml | 15 +++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/openapi/Swarm.yaml b/openapi/Swarm.yaml index 7f48e32d329..ce68c014a85 100644 --- a/openapi/Swarm.yaml +++ b/openapi/Swarm.yaml @@ -435,7 +435,6 @@ paths: $ref: "SwarmCommon.yaml#/components/schemas/SwarmReference" required: true description: Swarm address of content - - $ref: "SwarmCommon.yaml#/components/parameters/SwarmFeedLegacyResolve" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmCache" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyStrategyParameter" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyFallbackModeParameter" @@ -454,6 +453,11 @@ paths: schema: type: string format: binary + headers: + "swarm-feed-resolved-version": + $ref: "SwarmCommon.yaml#/components/headers/SwarmFeedResolvedVersion" + + "400": $ref: "SwarmCommon.yaml#/components/responses/400" "404": @@ -504,7 +508,6 @@ paths: type: string required: true description: Path to the file in the collection. - - $ref: "SwarmCommon.yaml#/components/parameters/SwarmFeedLegacyResolve" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyStrategyParameter" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyFallbackModeParameter" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmChunkRetrievalTimeoutParameter" @@ -516,6 +519,9 @@ paths: schema: type: string format: binary + headers: + "swarm-feed-resolved-version": + $ref: "SwarmCommon.yaml#/components/headers/SwarmFeedResolvedVersion" "400": $ref: "SwarmCommon.yaml#/components/responses/400" @@ -1032,7 +1038,6 @@ paths: $ref: "SwarmCommon.yaml#/components/schemas/FeedType" required: false description: "Feed indexing scheme (default: sequence)" - - $ref: "SwarmCommon.yaml#/components/parameters/SwarmFeedLegacyResolve" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmOnlyRootChunkParameter" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmCache" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyStrategyParameter" @@ -1048,6 +1053,9 @@ paths: $ref: "SwarmCommon.yaml#/components/headers/SwarmFeedIndex" "swarm-feed-index-next": $ref: "SwarmCommon.yaml#/components/headers/SwarmFeedIndexNext" + "swarm-feed-resolved-version": + $ref: "SwarmCommon.yaml#/components/headers/SwarmFeedResolvedVersion" + content: application/octet-stream: schema: diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index bf15dbf205e..1c96d34bf7d 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -1071,6 +1071,13 @@ components: schema: type: string + SwarmFeedResolvedVersion: + schema: + type: string + required: false + description: "Indicates which feed version was resolved (v1 or v2)" + + parameters: GasPriceParameter: in: header @@ -1276,14 +1283,6 @@ components: required: false description: "ACT history Unix timestamp" - SwarmFeedLegacyResolve: - in: query - name: swarm-feed-legacy-resolve - schema: - type: boolean - required: false - description: "Resolves feed payloads in legacy structure (timestamp, content address)." - responses: "200": description: OK. From 4cd9bd580bd79b4474df4a6d4ad6315d91c97f3b Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:50:35 -0600 Subject: [PATCH 11/15] chore: rename stuff --- pkg/api/bzz.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index 2e16a607910..e547f6f951d 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -419,15 +419,15 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa // should fetch both feed versions. if the length isn't v1 // then we should only try to fetch v2. var ( - v1, v2 chan getWrappedResult - both = false + v1C, v2C chan getWrappedResult + both = false ) if isV1 { both = true - v1 = getWrapped(true) - v2 = getWrapped(false) + v1C = getWrapped(true) + v2C = getWrapped(false) } else { - v2 = getWrapped(false) + v2C = getWrapped(false) } // closure to handle processing one channel then the other. @@ -464,10 +464,10 @@ func (s *Service) resolveFeed(ctx context.Context, getter storage.Getter, ch swa } } select { - case v1r := <-v1: - return processChanOutput("v1", v1r, v2) - case v2r := <-v2: - return processChanOutput("v2", v2r, v1) + case v1r := <-v1C: + return processChanOutput("v1", v1r, v2C) + case v2r := <-v2C: + return processChanOutput("v2", v2r, v1C) case <-innerCtx.Done(): return nil, "", ctx.Err() } From cd4da09e91008f6dea4b1f8559c4f82daeb50a75 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:16:16 -0600 Subject: [PATCH 12/15] ci: add feed tests --- .github/workflows/beekeeper.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/beekeeper.yml b/.github/workflows/beekeeper.yml index 4c69fce7a6f..a2084e934b2 100644 --- a/.github/workflows/beekeeper.yml +++ b/.github/workflows/beekeeper.yml @@ -14,7 +14,7 @@ env: SETUP_CONTRACT_IMAGE: "ethersphere/bee-localchain" SETUP_CONTRACT_IMAGE_TAG: "0.9.4" BEELOCAL_BRANCH: "main" - BEEKEEPER_BRANCH: "master" + BEEKEEPER_BRANCH: "v1-feed" BEEKEEPER_METRICS_ENABLED: false REACHABILITY_OVERRIDE_PUBLIC: true BATCHFACTOR_OVERRIDE_PUBLIC: 2 @@ -158,6 +158,9 @@ jobs: - name: Test manifest id: manifest run: timeout ${TIMEOUT} beekeeper check --cluster-name local-dns --checks=ci-manifest + - name: Test manifest v1 + id: manifest-v1 + run: timeout ${TIMEOUT} beekeeper check --cluster-name local-dns --checks=ci-manifest-v1 - name: Test postage stamps id: postage-stamps run: timeout ${TIMEOUT} beekeeper check --cluster-name local-dns --checks ci-postage @@ -173,6 +176,9 @@ jobs: - name: Test act id: act run: timeout ${TIMEOUT} bash -c 'until beekeeper check --cluster-name local-dns --checks ci-act; do echo "waiting for act..."; sleep .3; done' + - name: Test feeds v1 + id: feeds-v1 + run: timeout ${TIMEOUT} beekeeper check --cluster-name local-dns --checks=ci-feed-v1 - name: Test feeds id: feeds run: timeout ${TIMEOUT} beekeeper check --cluster-name local-dns --checks=ci-feed @@ -190,6 +196,9 @@ jobs: if ${{ steps.pushsync-chunks-2.outcome=='failure' }}; then FAILED=pushsync-chunks-2; fi if ${{ steps.retrieval.outcome=='failure' }}; then FAILED=retrieval; fi if ${{ steps.manifest.outcome=='failure' }}; then FAILED=manifest; fi + if ${{ steps.manifest-v1.outcome=='failure' }}; then FAILED=manifest-v1; fi + if ${{ steps.feeds.outcome=='failure' }}; then FAILED=feeds; fi + if ${{ steps.feeds-v1.outcome=='failure' }}; then FAILED=feeds-v1; fi if ${{ steps.act.outcome=='failure' }}; then FAILED=act; fi curl -sSf -X POST -H "Content-Type: application/json" -d "{\"text\": \"**${RUN_TYPE}** Beekeeper Error\nBranch: \`${{ github.head_ref }}\`\nUser: @${{ github.event.pull_request.user.login }}\nDebugging artifacts: [click](https://$BUCKET_NAME.$AWS_ENDPOINT/artifacts_$VERTAG.tar.gz)\nStep failed: \`${FAILED}\`\"}" https://beehive.ethswarm.org/hooks/${{ secrets.TUNSHELL_KEY }} echo "Failed test: ${FAILED}" From 080de51676d0dd1ea20ef26137fba232c359b9e4 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Fri, 2 Jan 2026 07:56:14 -0600 Subject: [PATCH 13/15] chore: switch beekeeper to master again --- .github/workflows/beekeeper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beekeeper.yml b/.github/workflows/beekeeper.yml index a2084e934b2..43ac9b18c37 100644 --- a/.github/workflows/beekeeper.yml +++ b/.github/workflows/beekeeper.yml @@ -14,7 +14,7 @@ env: SETUP_CONTRACT_IMAGE: "ethersphere/bee-localchain" SETUP_CONTRACT_IMAGE_TAG: "0.9.4" BEELOCAL_BRANCH: "main" - BEEKEEPER_BRANCH: "v1-feed" + BEEKEEPER_BRANCH: "master" BEEKEEPER_METRICS_ENABLED: false REACHABILITY_OVERRIDE_PUBLIC: true BATCHFACTOR_OVERRIDE_PUBLIC: 2 From 21d788f519f6f9784020b7c5eae0a6c89abdcca9 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Sat, 3 Jan 2026 08:18:46 -0600 Subject: [PATCH 14/15] chore: change back to feed branch --- .github/workflows/beekeeper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beekeeper.yml b/.github/workflows/beekeeper.yml index 43ac9b18c37..a2084e934b2 100644 --- a/.github/workflows/beekeeper.yml +++ b/.github/workflows/beekeeper.yml @@ -14,7 +14,7 @@ env: SETUP_CONTRACT_IMAGE: "ethersphere/bee-localchain" SETUP_CONTRACT_IMAGE_TAG: "0.9.4" BEELOCAL_BRANCH: "main" - BEEKEEPER_BRANCH: "master" + BEEKEEPER_BRANCH: "v1-feed" BEEKEEPER_METRICS_ENABLED: false REACHABILITY_OVERRIDE_PUBLIC: true BATCHFACTOR_OVERRIDE_PUBLIC: 2 From 615d3ee953abcd8763d5af223ff224d7e9a8a1b9 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:19:49 -0600 Subject: [PATCH 15/15] chore: try again to revert to master --- .github/workflows/beekeeper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beekeeper.yml b/.github/workflows/beekeeper.yml index a2084e934b2..43ac9b18c37 100644 --- a/.github/workflows/beekeeper.yml +++ b/.github/workflows/beekeeper.yml @@ -14,7 +14,7 @@ env: SETUP_CONTRACT_IMAGE: "ethersphere/bee-localchain" SETUP_CONTRACT_IMAGE_TAG: "0.9.4" BEELOCAL_BRANCH: "main" - BEEKEEPER_BRANCH: "v1-feed" + BEEKEEPER_BRANCH: "master" BEEKEEPER_METRICS_ENABLED: false REACHABILITY_OVERRIDE_PUBLIC: true BATCHFACTOR_OVERRIDE_PUBLIC: 2