Skip to content

Commit 71c4fb9

Browse files
authored
feat: feed legacy payload param (#5163)
1 parent 364fbc5 commit 71c4fb9

File tree

10 files changed

+215
-72
lines changed

10 files changed

+215
-72
lines changed

openapi/Swarm.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ paths:
435435
$ref: "SwarmCommon.yaml#/components/schemas/SwarmReference"
436436
required: true
437437
description: Swarm address of content
438+
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmFeedLegacyResolve"
438439
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmCache"
439440
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyStrategyParameter"
440441
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyFallbackModeParameter"
@@ -503,6 +504,7 @@ paths:
503504
type: string
504505
required: true
505506
description: Path to the file in the collection.
507+
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmFeedLegacyResolve"
506508
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyStrategyParameter"
507509
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyFallbackModeParameter"
508510
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmChunkRetrievalTimeoutParameter"
@@ -1036,6 +1038,7 @@ paths:
10361038
$ref: "SwarmCommon.yaml#/components/schemas/FeedType"
10371039
required: false
10381040
description: "Feed indexing scheme (default: sequence)"
1041+
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmFeedLegacyResolve"
10391042
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmOnlyRootChunkParameter"
10401043
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmCache"
10411044
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyStrategyParameter"

openapi/SwarmCommon.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,14 @@ components:
12821282
required: false
12831283
description: "ACT history Unix timestamp"
12841284

1285+
SwarmFeedLegacyResolve:
1286+
in: query
1287+
name: swarm-feed-legacy-resolve
1288+
schema:
1289+
type: boolean
1290+
required: false
1291+
description: "Resolves feed payloads in legacy structure (timestamp, content address)."
1292+
12851293
responses:
12861294
"200":
12871295
description: OK.

pkg/api/bzz.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,15 @@ func (s *Service) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
337337
paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some.
338338
}
339339

340-
s.serveReference(logger, address, paths.Path, w, r, false)
340+
queries := struct {
341+
FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"`
342+
}{}
343+
if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
344+
response("invalid query params", logger, w)
345+
return
346+
}
347+
348+
s.serveReference(logger, address, paths.Path, w, r, false, queries.FeedLegacyResolve)
341349
}
342350

343351
func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) {
@@ -352,6 +360,14 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) {
352360
return
353361
}
354362

363+
queries := struct {
364+
FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"`
365+
}{}
366+
if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
367+
response("invalid query params", logger, w)
368+
return
369+
}
370+
355371
address := paths.Address
356372
if v := getAddressFromContext(r.Context()); !v.IsZero() {
357373
address = v
@@ -361,10 +377,10 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) {
361377
paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some.
362378
}
363379

364-
s.serveReference(logger, address, paths.Path, w, r, true)
380+
s.serveReference(logger, address, paths.Path, w, r, true, queries.FeedLegacyResolve)
365381
}
366382

367-
func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathVar string, w http.ResponseWriter, r *http.Request, headerOnly bool) {
383+
func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathVar string, w http.ResponseWriter, r *http.Request, headerOnly bool, feedLegacyResolve bool) {
368384
loggerV1 := logger.V(1).Build()
369385

370386
headers := struct {
@@ -433,8 +449,20 @@ FETCH:
433449
jsonhttp.NotFound(w, "no update found")
434450
return
435451
}
436-
wc, err := feeds.GetWrappedChunk(ctx, s.storer.Download(cache), ch)
452+
wc, err := feeds.GetWrappedChunk(ctx, s.storer.Download(cache), ch, feedLegacyResolve)
437453
if err != nil {
454+
if errors.Is(err, feeds.ErrNotLegacyPayload) {
455+
logger.Debug("bzz: download: feed is not a legacy payload")
456+
logger.Error(err, "bzz download: feed is not a legacy payload")
457+
jsonhttp.BadRequest(w, "bzz download: feed is not a legacy payload")
458+
return
459+
}
460+
if errors.As(err, &feeds.WrappedChunkNotFoundError{}) {
461+
logger.Debug("bzz download: feed pointing to the wrapped chunk not found", "error", err)
462+
logger.Error(err, "bzz download: feed pointing to the wrapped chunk not found")
463+
jsonhttp.NotFound(w, "bzz download: feed pointing to the wrapped chunk not found")
464+
return
465+
}
438466
logger.Debug("bzz download: mapStructure feed update failed", "error", err)
439467
logger.Error(nil, "bzz download: mapStructure feed update failed")
440468
jsonhttp.InternalServerError(w, "mapStructure feed update")

pkg/api/bzz_test.go

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"mime"
1414
"mime/multipart"
1515
"net/http"
16+
"net/url"
1617
"strconv"
1718
"strings"
1819
"testing"
@@ -26,6 +27,7 @@ import (
2627
"github.com/ethersphere/bee/v2/pkg/manifest"
2728
mockbatchstore "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock"
2829
mockpost "github.com/ethersphere/bee/v2/pkg/postage/mock"
30+
testingsoc "github.com/ethersphere/bee/v2/pkg/soc/testing"
2931
"github.com/ethersphere/bee/v2/pkg/storage/inmemchunkstore"
3032
mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock"
3133
"github.com/ethersphere/bee/v2/pkg/swarm"
@@ -759,11 +761,24 @@ func TestFeedIndirection(t *testing.T) {
759761
updateData = []byte("<h1>Swarm Feeds Hello World!</h1>")
760762
logger = log.Noop
761763
storer = mockstorer.New()
764+
ctx = context.Background()
762765
client, _, _, _ = newTestServer(t, testServerOptions{
763766
Storer: storer,
764767
Logger: logger,
765768
Post: mockpost.New(mockpost.WithAcceptAll()),
766769
})
770+
bzzDownloadResource = func(addr, path string, legacyFeed bool) string {
771+
values := url.Values{}
772+
if legacyFeed {
773+
values.Set("swarm-feed-legacy-resolve", strconv.FormatBool(legacyFeed))
774+
}
775+
776+
baseURL := "/bzz/" + addr + "/" + path
777+
if len(values) > 0 {
778+
return baseURL + "?" + values.Encode()
779+
}
780+
return baseURL
781+
}
767782
)
768783
// tar all the test case files
769784
tarReader := tarFiles(t, []f{
@@ -794,29 +809,6 @@ func TestFeedIndirection(t *testing.T) {
794809
t.Fatalf("expected file reference, did not got any")
795810
}
796811

797-
// now use the "content" to mock the feed lookup
798-
// also, use the mocked mantaray chunks that unmarshal
799-
// into a real manifest with the mocked feed values when
800-
// called from the bzz endpoint. then call the bzz endpoint with
801-
// the pregenerated feed root manifest hash
802-
803-
feedUpdate := toChunk(t, 121212, resp.Reference.Bytes())
804-
805-
var (
806-
look = newMockLookup(-1, 0, feedUpdate, nil, &id{}, nil)
807-
factory = newMockFactory(look)
808-
bzzDownloadResource = func(addr, path string) string { return "/bzz/" + addr + "/" + path }
809-
ctx = context.Background()
810-
)
811-
client, _, _, _ = newTestServer(t, testServerOptions{
812-
Storer: storer,
813-
Logger: logger,
814-
Feeds: factory,
815-
})
816-
err := storer.Cache().Put(ctx, feedUpdate)
817-
if err != nil {
818-
t.Fatal(err)
819-
}
820812
m, err := manifest.NewDefaultManifest(
821813
loadsave.New(storer.ChunkStore(), storer.Cache(), pipelineFactory(storer.Cache(), false, 0), redundancy.DefaultLevel),
822814
false,
@@ -838,14 +830,66 @@ func TestFeedIndirection(t *testing.T) {
838830
t.Fatal(err)
839831
}
840832

841-
jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), ""), http.StatusOK,
842-
jsonhttptest.WithExpectedResponse(updateData),
843-
jsonhttptest.WithExpectedContentLength(len(updateData)),
844-
jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader),
845-
jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader),
846-
jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, `inline; filename="index.html"`),
847-
jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"),
848-
)
833+
// now use the "content" root chunk to mock the feed lookup
834+
// also, use the mocked mantaray chunks that unmarshal
835+
// into a real manifest with the mocked feed values when
836+
// called from the bzz endpoint. then call the bzz endpoint with
837+
// the pregenerated feed root manifest hash
838+
839+
t.Run("legacy feed", func(t *testing.T) {
840+
feedUpdate := toChunk(t, 121212, resp.Reference.Bytes())
841+
842+
var (
843+
look = newMockLookup(-1, 0, feedUpdate, nil, &id{}, nil)
844+
factory = newMockFactory(look)
845+
)
846+
client, _, _, _ = newTestServer(t, testServerOptions{
847+
Storer: storer,
848+
Logger: logger,
849+
Feeds: factory,
850+
})
851+
852+
jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", true), http.StatusOK,
853+
jsonhttptest.WithExpectedResponse(updateData),
854+
jsonhttptest.WithExpectedContentLength(len(updateData)),
855+
jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader),
856+
jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader),
857+
jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, `inline; filename="index.html"`),
858+
jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"),
859+
)
860+
861+
jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusNotFound)
862+
})
863+
864+
t.Run("wrapped feed", func(t *testing.T) {
865+
// get root chunk of data and wrap it in a feed
866+
rootCh, err := storer.ChunkStore().Get(ctx, resp.Reference)
867+
if err != nil {
868+
t.Fatal(err)
869+
}
870+
socRootCh := testingsoc.GenerateMockSOC(t, rootCh.Data()[swarm.SpanSize:]).Chunk()
871+
872+
var (
873+
look = newMockLookup(-1, 0, socRootCh, nil, &id{}, nil)
874+
factory = newMockFactory(look)
875+
)
876+
client, _, _, _ = newTestServer(t, testServerOptions{
877+
Storer: storer,
878+
Logger: logger,
879+
Feeds: factory,
880+
})
881+
882+
jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusOK,
883+
jsonhttptest.WithExpectedResponse(updateData),
884+
jsonhttptest.WithExpectedContentLength(len(updateData)),
885+
jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader),
886+
jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader),
887+
jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, `inline; filename="index.html"`),
888+
jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"),
889+
)
890+
891+
jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", true), http.StatusBadRequest)
892+
})
849893
}
850894

851895
func Test_bzzDownloadHandler_invalidInputs(t *testing.T) {

pkg/api/feed.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) {
5353
}
5454

5555
queries := struct {
56-
At int64 `map:"at"`
57-
After uint64 `map:"after"`
56+
At int64 `map:"at"`
57+
After uint64 `map:"after"`
58+
FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"`
5859
}{}
5960
if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
6061
response("invalid query params", logger, w)
@@ -102,7 +103,7 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) {
102103
return
103104
}
104105

105-
wc, err := feeds.GetWrappedChunk(r.Context(), s.storer.Download(false), ch)
106+
wc, err := feeds.GetWrappedChunk(r.Context(), s.storer.Download(false), ch, queries.FeedLegacyResolve)
106107
if err != nil {
107108
logger.Error(nil, "wrapped chunk cannot be retrieved")
108109
jsonhttp.NotFound(w, "wrapped chunk cannot be retrieved")

pkg/api/feed_test.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"io"
1515
"math/big"
1616
"net/http"
17+
"net/url"
18+
"strconv"
1719
"testing"
1820

1921
"github.com/ethersphere/bee/v2/pkg/api"
@@ -42,11 +44,20 @@ func TestFeed_Get(t *testing.T) {
4244
t.Parallel()
4345

4446
var (
45-
feedResource = func(owner, topic, at string) string {
47+
feedResource = func(owner, topic, at string, legacyFeed bool) string {
48+
values := url.Values{}
4649
if at != "" {
47-
return fmt.Sprintf("/feeds/%s/%s?at=%s", owner, topic, at)
50+
values.Set("at", at)
4851
}
49-
return fmt.Sprintf("/feeds/%s/%s", owner, topic)
52+
if legacyFeed {
53+
values.Set("swarm-feed-legacy-resolve", strconv.FormatBool(legacyFeed))
54+
}
55+
56+
baseURL := fmt.Sprintf("/feeds/%s/%s", owner, topic)
57+
if len(values) > 0 {
58+
return baseURL + "?" + values.Encode()
59+
}
60+
return baseURL
5061
}
5162
mockStorer = mockstorer.New()
5263
)
@@ -75,7 +86,7 @@ func TestFeed_Get(t *testing.T) {
7586
})
7687
)
7788

78-
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "12"), http.StatusOK,
89+
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "12", true), http.StatusOK,
7990
jsonhttptest.WithExpectedResponse(mockWrappedCh.Data()[swarm.SpanSize:]),
8091
jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)),
8192
jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader),
@@ -86,7 +97,7 @@ func TestFeed_Get(t *testing.T) {
8697
)
8798
})
8899

89-
t.Run("latest", func(t *testing.T) {
100+
t.Run("latest with legacy payload", func(t *testing.T) {
90101
t.Parallel()
91102

92103
var (
@@ -102,7 +113,7 @@ func TestFeed_Get(t *testing.T) {
102113
})
103114
)
104115

105-
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK,
116+
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", true), http.StatusOK,
106117
jsonhttptest.WithExpectedResponse(mockWrappedCh.Data()[swarm.SpanSize:]),
107118
jsonhttptest.WithExpectedContentLength(len(mockWrappedCh.Data()[swarm.SpanSize:])),
108119
jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)),
@@ -131,7 +142,7 @@ func TestFeed_Get(t *testing.T) {
131142
})
132143
)
133144

134-
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK,
145+
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusOK,
135146
jsonhttptest.WithExpectedResponse(testData),
136147
jsonhttptest.WithExpectedContentLength(len(testData)),
137148
jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)),
@@ -161,7 +172,23 @@ func TestFeed_Get(t *testing.T) {
161172
})
162173
)
163174

164-
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusNotFound)
175+
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", true), http.StatusNotFound)
176+
})
177+
178+
t.Run("query parameter legacy feed resolve", func(t *testing.T) {
179+
t.Parallel()
180+
181+
var (
182+
look = newMockLookup(1, 0, nil, errors.New("dummy"), &id{}, &id{})
183+
factory = newMockFactory(look)
184+
client, _, _, _ = newTestServer(t, testServerOptions{
185+
Storer: mockStorer,
186+
Feeds: factory,
187+
})
188+
)
189+
190+
// Test with the legacyFeed parameter set to true which should add the query parameter
191+
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusNotFound)
165192
})
166193

167194
t.Run("bigger payload than one chunk", func(t *testing.T) {
@@ -193,7 +220,7 @@ func TestFeed_Get(t *testing.T) {
193220
)
194221

195222
t.Run("retrieve chunk tree", func(t *testing.T) {
196-
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK,
223+
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusOK,
197224
jsonhttptest.WithExpectedResponse(testData),
198225
jsonhttptest.WithExpectedContentLength(testDataLen),
199226
jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)),
@@ -206,7 +233,7 @@ func TestFeed_Get(t *testing.T) {
206233
})
207234

208235
t.Run("retrieve only wrapped chunk", func(t *testing.T) {
209-
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK,
236+
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusOK,
210237
jsonhttptest.WithRequestHeader(api.SwarmOnlyRootChunk, "true"),
211238
jsonhttptest.WithExpectedResponse(testRootCh.Data()),
212239
jsonhttptest.WithExpectedContentLength(len(testRootCh.Data())),

pkg/api/subdomain.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,13 @@ func (s *Service) subdomainHandler(w http.ResponseWriter, r *http.Request) {
2828
paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some.
2929
}
3030

31-
s.serveReference(logger, paths.Subdomain, paths.Path, w, r, false)
31+
queries := struct {
32+
FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"`
33+
}{}
34+
if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
35+
response("invalid query params", logger, w)
36+
return
37+
}
38+
39+
s.serveReference(logger, paths.Subdomain, paths.Path, w, r, false, queries.FeedLegacyResolve)
3240
}

0 commit comments

Comments
 (0)