From 1fe41f2f6f8f7318c290394f0df6731c8604a936 Mon Sep 17 00:00:00 2001 From: akrem-chabchoub Date: Fri, 2 Jan 2026 19:03:39 +0100 Subject: [PATCH 1/9] fix: add validation for redundancy level in bytes and bzz upload handlers --- pkg/api/bytes.go | 2 +- pkg/api/bzz.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/api/bytes.go b/pkg/api/bytes.go index 4c1cd891df0..72e785e378c 100644 --- a/pkg/api/bytes.go +++ b/pkg/api/bytes.go @@ -40,7 +40,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { Pin bool `map:"Swarm-Pin"` Deferred *bool `map:"Swarm-Deferred-Upload"` Encrypt bool `map:"Swarm-Encrypt"` - RLevel redundancy.Level `map:"Swarm-Redundancy-Level"` + RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"lte=4"` Act bool `map:"Swarm-Act"` HistoryAddress swarm.Address `map:"Swarm-Act-History-Address"` }{} diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index a27e3f813c5..76f04eef4c3 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -72,7 +72,7 @@ func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) { Deferred *bool `map:"Swarm-Deferred-Upload"` Encrypt bool `map:"Swarm-Encrypt"` IsDir bool `map:"Swarm-Collection"` - RLevel redundancy.Level `map:"Swarm-Redundancy-Level"` + RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"lte=4"` Act bool `map:"Swarm-Act"` HistoryAddress swarm.Address `map:"Swarm-Act-History-Address"` }{} @@ -387,7 +387,7 @@ func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathV Cache *bool `map:"Swarm-Cache"` Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"` FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"` - RLevel *redundancy.Level `map:"Swarm-Redundancy-Level"` + RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"lte=4"` ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"` }{} @@ -599,7 +599,7 @@ func (s *Service) serveManifestEntry( func (s *Service) downloadHandler(logger log.Logger, w http.ResponseWriter, r *http.Request, reference swarm.Address, additionalHeaders http.Header, etag, headersOnly bool, rootCh swarm.Chunk) { headers := struct { Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"` - RLevel *redundancy.Level `map:"Swarm-Redundancy-Level"` + RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"lte=4"` FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"` ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"` LookaheadBufferSize *int `map:"Swarm-Lookahead-Buffer-Size"` From 3fd49f8b632400af2be4994ab213a895a4394161 Mon Sep 17 00:00:00 2001 From: akrem-chabchoub Date: Fri, 2 Jan 2026 19:14:11 +0100 Subject: [PATCH 2/9] test(bzz): add unit tests for redundancy level validation in BZZ upload handler --- pkg/api/bzz_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index 7f636476d1f..9f9f7385551 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -1185,3 +1185,37 @@ func TestBzzDownloadHeaders(t *testing.T) { jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), ) } + +func TestBzzRedundancyLevel(t *testing.T) { + t.Parallel() + + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: mockstorer.New(), + Post: mockpost.New(mockpost.WithAcceptAll()), + }) + + tests := []struct { + name string + level string + wantStatus int + }{ + {"level 0 (NONE) is valid", "0", http.StatusCreated}, + {"level 1 (MEDIUM) is valid", "1", http.StatusCreated}, + {"level 2 (STRONG) is valid", "2", http.StatusCreated}, + {"level 3 (INSANE) is valid", "3", http.StatusCreated}, + {"level 4 (PARANOID) is valid", "4", http.StatusCreated}, + {"level 5 is invalid", "5", http.StatusBadRequest}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jsonhttptest.Request(t, client, http.MethodPost, "/bzz", tt.wantStatus, + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/plain"), + jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, tt.level), + jsonhttptest.WithRequestBody(bytes.NewReader([]byte("test"))), + ) + }) + } +} From c6d94e0a76b256aebfedc16c2974a117bbd1d9fe Mon Sep 17 00:00:00 2001 From: akrem-chabchoub Date: Fri, 2 Jan 2026 19:14:31 +0100 Subject: [PATCH 3/9] test(bytes): implement unit tests for redundancy level validation in bytes upload handler --- pkg/api/bytes_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pkg/api/bytes_test.go b/pkg/api/bytes_test.go index 40280e44992..0cfc0289c69 100644 --- a/pkg/api/bytes_test.go +++ b/pkg/api/bytes_test.go @@ -408,3 +408,36 @@ func TestBytesDirectUpload(t *testing.T) { }), ) } + +func TestBytesRedundancyLevel(t *testing.T) { + t.Parallel() + + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: mockstorer.New(), + Post: mockpost.New(mockpost.WithAcceptAll()), + }) + + tests := []struct { + name string + level string + wantStatus int + }{ + {"level 0 (NONE) is valid", "0", http.StatusCreated}, + {"level 1 (MEDIUM) is valid", "1", http.StatusCreated}, + {"level 2 (STRONG) is valid", "2", http.StatusCreated}, + {"level 3 (INSANE) is valid", "3", http.StatusCreated}, + {"level 4 (PARANOID) is valid", "4", http.StatusCreated}, + {"level 5 is invalid", "5", http.StatusBadRequest}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jsonhttptest.Request(t, client, http.MethodPost, "/bytes", tt.wantStatus, + jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, tt.level), + jsonhttptest.WithRequestBody(bytes.NewReader([]byte("test"))), + ) + }) + } +} From e5b93cceb221ed1219dfe88c2d1d138680156ede Mon Sep 17 00:00:00 2001 From: akrem-chabchoub Date: Sun, 4 Jan 2026 18:32:59 +0100 Subject: [PATCH 4/9] fix(bzz): add omitempty to redundancy level validation in download and serveReference --- pkg/api/bzz.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index 76f04eef4c3..fb461d78ef0 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -387,7 +387,7 @@ func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathV Cache *bool `map:"Swarm-Cache"` Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"` FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"` - RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"lte=4"` + RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,lte=4"` ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"` }{} @@ -599,7 +599,7 @@ func (s *Service) serveManifestEntry( func (s *Service) downloadHandler(logger log.Logger, w http.ResponseWriter, r *http.Request, reference swarm.Address, additionalHeaders http.Header, etag, headersOnly bool, rootCh swarm.Chunk) { headers := struct { Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"` - RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"lte=4"` + RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,lte=4"` FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"` ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"` LookaheadBufferSize *int `map:"Swarm-Lookahead-Buffer-Size"` From 8e2cd27d4abb7a65ebadbd3a8a9305aa2232b150 Mon Sep 17 00:00:00 2001 From: akrem-chabchoub Date: Sun, 4 Jan 2026 23:31:53 +0100 Subject: [PATCH 5/9] refactor(docs): simplify header parameter definitions in /bytes endpoint --- openapi/Swarm.yaml | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/openapi/Swarm.yaml b/openapi/Swarm.yaml index 7f48e32d329..309d9fc11d9 100644 --- a/openapi/Swarm.yaml +++ b/openapi/Swarm.yaml @@ -163,37 +163,12 @@ paths: tags: - Bytes parameters: - - in: header - schema: - $ref: "SwarmCommon.yaml#/components/parameters/SwarmPostageBatchId" - name: swarm-postage-batch-id - required: true - - in: header - schema: - $ref: "SwarmCommon.yaml#/components/parameters/SwarmTagParameter" - name: swarm-tag - required: false - - in: header - schema: - $ref: "SwarmCommon.yaml#/components/parameters/SwarmPinParameter" - name: swarm-pin - required: false - - in: header - schema: - $ref: "SwarmCommon.yaml#/components/parameters/SwarmDeferredUpload" - name: swarm-deferred-upload - required: false - - in: header - schema: - $ref: "SwarmCommon.yaml#/components/parameters/SwarmEncryptParameter" - name: swarm-encrypt - required: false - - in: header - schema: - $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyLevelParameter" - name: swarm-redundancy-level - required: false - + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmPostageBatchId" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmTagParameter" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmPinParameter" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmDeferredUpload" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmEncryptParameter" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyLevelParameter" requestBody: content: application/octet-stream: From b7d9bccc6df345878192c29e2a25fb9fe7ec2ff0 Mon Sep 17 00:00:00 2001 From: akrem-chabchoub Date: Mon, 5 Jan 2026 09:19:27 +0100 Subject: [PATCH 6/9] test(bzz): enhance redundancy level tests for upload and download handlers --- pkg/api/bzz_test.go | 133 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 120 insertions(+), 13 deletions(-) diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index 9f9f7385551..6a338fa4a57 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -1186,7 +1186,7 @@ func TestBzzDownloadHeaders(t *testing.T) { ) } -func TestBzzRedundancyLevel(t *testing.T) { +func TestBzzUploadRedundancyLevel(t *testing.T) { t.Parallel() client, _, _, _ := newTestServer(t, testServerOptions{ @@ -1194,28 +1194,135 @@ func TestBzzRedundancyLevel(t *testing.T) { Post: mockpost.New(mockpost.WithAcceptAll()), }) + const maxValidLevel = redundancy.PARANOID + tests := []struct { - name string - level string - wantStatus int + name string + level int + want *jsonhttp.StatusResponse }{ - {"level 0 (NONE) is valid", "0", http.StatusCreated}, - {"level 1 (MEDIUM) is valid", "1", http.StatusCreated}, - {"level 2 (STRONG) is valid", "2", http.StatusCreated}, - {"level 3 (INSANE) is valid", "3", http.StatusCreated}, - {"level 4 (PARANOID) is valid", "4", http.StatusCreated}, - {"level 5 is invalid", "5", http.StatusBadRequest}, + {"minimum level (NONE) is valid", int(redundancy.NONE), nil}, + {"maximum valid level (PARANOID) is valid", int(maxValidLevel), nil}, + { + "level below minimum is invalid", int(-1), + &jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid header params", + Reasons: []jsonhttp.Reason{ + { + Field: "Swarm-Redundancy-Level", + Error: "invalid syntax", + }, + }, + }, + }, + { + "level above maximum is invalid", int(maxValidLevel + 1), + &jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid header params", + Reasons: []jsonhttp.Reason{ + { + Field: "swarm-redundancy-level", + Error: fmt.Sprintf("want lte:%d", maxValidLevel), + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - jsonhttptest.Request(t, client, http.MethodPost, "/bzz", tt.wantStatus, + opts := []jsonhttptest.Option{ jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/plain"), jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), - jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, tt.level), + jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, strconv.Itoa(tt.level)), jsonhttptest.WithRequestBody(bytes.NewReader([]byte("test"))), - ) + } + var statusCode int + if tt.want == nil { + statusCode = http.StatusCreated + } else { + statusCode = tt.want.Code + opts = append(opts, jsonhttptest.WithExpectedJSONResponse(*tt.want)) + } + jsonhttptest.Request(t, client, http.MethodPost, "/bzz", statusCode, opts...) + }) + } +} + +func TestBzzDownloadRedundancyLevel(t *testing.T) { + t.Parallel() + + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: mockstorer.New(), + Post: mockpost.New(mockpost.WithAcceptAll()), + }) + + testData := []byte("test download redundancy level") + var resp api.BzzUploadResponse + jsonhttptest.Request(t, client, http.MethodPost, "/bzz", http.StatusCreated, + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/plain"), + jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestBody(bytes.NewReader(testData)), + jsonhttptest.WithUnmarshalJSONResponse(&resp), + ) + + const maxValidLevel = redundancy.PARANOID + + tests := []struct { + name string + level int + want *jsonhttp.StatusResponse + }{ + {"minimum level (NONE) is valid", int(redundancy.NONE), nil}, + {"maximum valid level (PARANOID) is valid", int(maxValidLevel), nil}, + { + "level below minimum is invalid", int(-1), + &jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid header params", + Reasons: []jsonhttp.Reason{ + { + Field: "Swarm-Redundancy-Level", + Error: "invalid syntax", + }, + }, + }, + }, + { + "level above maximum is invalid", int(maxValidLevel + 1), + &jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid header params", + Reasons: []jsonhttp.Reason{ + { + Field: "swarm-redundancy-level", + Error: fmt.Sprintf("want lte:%d", maxValidLevel), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := []jsonhttptest.Option{ + jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, strconv.Itoa(tt.level)), + } + var statusCode int + if tt.want == nil { + statusCode = http.StatusOK + opts = append(opts, + jsonhttptest.WithExpectedResponse(testData), + ) + } else { + statusCode = tt.want.Code + opts = append(opts, jsonhttptest.WithExpectedJSONResponse(*tt.want)) + } + jsonhttptest.Request(t, client, http.MethodGet, "/bzz/"+resp.Reference.String(), statusCode, opts...) }) } } From aaf1753f7c94a0e09d9b1d4b12df11304924d381 Mon Sep 17 00:00:00 2001 From: akrem-chabchoub Date: Mon, 5 Jan 2026 16:43:51 +0100 Subject: [PATCH 7/9] test(bytes): improve redundancy level validation tests --- pkg/api/bytes_test.go | 58 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/pkg/api/bytes_test.go b/pkg/api/bytes_test.go index 0cfc0289c69..e91352fc63e 100644 --- a/pkg/api/bytes_test.go +++ b/pkg/api/bytes_test.go @@ -8,11 +8,13 @@ import ( "bytes" "context" "errors" + "fmt" "net/http" "strconv" "testing" "github.com/ethersphere/bee/v2/pkg/api" + "github.com/ethersphere/bee/v2/pkg/file/redundancy" "github.com/ethersphere/bee/v2/pkg/jsonhttp" "github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest" "github.com/ethersphere/bee/v2/pkg/log" @@ -417,27 +419,59 @@ func TestBytesRedundancyLevel(t *testing.T) { Post: mockpost.New(mockpost.WithAcceptAll()), }) + const maxValidLevel = redundancy.PARANOID + tests := []struct { - name string - level string - wantStatus int + name string + level int + want *jsonhttp.StatusResponse }{ - {"level 0 (NONE) is valid", "0", http.StatusCreated}, - {"level 1 (MEDIUM) is valid", "1", http.StatusCreated}, - {"level 2 (STRONG) is valid", "2", http.StatusCreated}, - {"level 3 (INSANE) is valid", "3", http.StatusCreated}, - {"level 4 (PARANOID) is valid", "4", http.StatusCreated}, - {"level 5 is invalid", "5", http.StatusBadRequest}, + {"minimum level (NONE) is valid", int(redundancy.NONE), nil}, + {"maximum valid level (PARANOID) is valid", int(maxValidLevel), nil}, + { + "level below minimum is invalid", int(-1), + &jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid header params", + Reasons: []jsonhttp.Reason{ + { + Field: "Swarm-Redundancy-Level", + Error: "invalid syntax", + }, + }, + }, + }, + { + "level above maximum is invalid", int(maxValidLevel + 1), + &jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid header params", + Reasons: []jsonhttp.Reason{ + { + Field: "swarm-redundancy-level", + Error: fmt.Sprintf("want lte:%d", maxValidLevel), + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - jsonhttptest.Request(t, client, http.MethodPost, "/bytes", tt.wantStatus, + opts := []jsonhttptest.Option{ jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), - jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, tt.level), + jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, strconv.Itoa(tt.level)), jsonhttptest.WithRequestBody(bytes.NewReader([]byte("test"))), - ) + } + var statusCode int + if tt.want == nil { + statusCode = http.StatusCreated + } else { + statusCode = tt.want.Code + opts = append(opts, jsonhttptest.WithExpectedJSONResponse(*tt.want)) + } + jsonhttptest.Request(t, client, http.MethodPost, "/bytes", statusCode, opts...) }) } } From 68a3cefb6c2b8837404276d1461874ab1de64fa1 Mon Sep 17 00:00:00 2001 From: akrem-chabchoub Date: Mon, 5 Jan 2026 18:42:15 +0100 Subject: [PATCH 8/9] feat(validation): implement custom validation for redundancy level with improved error messaging --- pkg/api/api.go | 14 +++++++++++--- pkg/api/bytes.go | 2 +- pkg/api/bytes_test.go | 2 +- pkg/api/bzz.go | 6 +++--- pkg/api/bzz_test.go | 2 +- pkg/api/validation.go | 30 ++++++++++++++++++++++++++++++ pkg/file/redundancy/level.go | 5 +++++ 7 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 pkg/api/validation.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 5f747237bc4..68a6e996b2b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -214,8 +214,9 @@ type Service struct { whitelistedWithdrawalAddress []common.Address - preMapHooks map[string]func(v string) (string, error) - validate *validator.Validate + preMapHooks map[string]func(v string) (string, error) + validationCustomErrorMessages map[string]func(err validator.FieldError) error + validate *validator.Validate redistributionAgent *storageincentives.Agent @@ -321,6 +322,7 @@ func New( } return name }) + s.setupValidation() s.stamperStore = stamperStore for _, v := range whitelistedWithdrawalAddress { @@ -709,11 +711,17 @@ func (s *Service) mapStructure(input, output any) func(string, log.Logger, http. case []byte: val = string(v) } + var cause error + if msgFn, ok := s.validationCustomErrorMessages[err.Tag()]; ok { + cause = msgFn(err) + } else { + cause = fmt.Errorf("want %s:%s", err.Tag(), err.Param()) + } vErrs = multierror.Append(vErrs, &validationError{ Entry: strings.ToLower(err.Field()), Value: val, - Cause: fmt.Errorf("want %s:%s", err.Tag(), err.Param()), + Cause: cause, }) } return response(vErrs.ErrorOrNil()) diff --git a/pkg/api/bytes.go b/pkg/api/bytes.go index 72e785e378c..945e9c3beae 100644 --- a/pkg/api/bytes.go +++ b/pkg/api/bytes.go @@ -40,7 +40,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { Pin bool `map:"Swarm-Pin"` Deferred *bool `map:"Swarm-Deferred-Upload"` Encrypt bool `map:"Swarm-Encrypt"` - RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"lte=4"` + RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"rLevel"` Act bool `map:"Swarm-Act"` HistoryAddress swarm.Address `map:"Swarm-Act-History-Address"` }{} diff --git a/pkg/api/bytes_test.go b/pkg/api/bytes_test.go index e91352fc63e..00271e8df99 100644 --- a/pkg/api/bytes_test.go +++ b/pkg/api/bytes_test.go @@ -449,7 +449,7 @@ func TestBytesRedundancyLevel(t *testing.T) { Reasons: []jsonhttp.Reason{ { Field: "swarm-redundancy-level", - Error: fmt.Sprintf("want lte:%d", maxValidLevel), + Error: fmt.Sprintf("want redundancy level to be in the range of %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)), }, }, }, diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index fb461d78ef0..92472efddcb 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -72,7 +72,7 @@ func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) { Deferred *bool `map:"Swarm-Deferred-Upload"` Encrypt bool `map:"Swarm-Encrypt"` IsDir bool `map:"Swarm-Collection"` - RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"lte=4"` + RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"rLevel"` Act bool `map:"Swarm-Act"` HistoryAddress swarm.Address `map:"Swarm-Act-History-Address"` }{} @@ -387,7 +387,7 @@ func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathV Cache *bool `map:"Swarm-Cache"` Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"` FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"` - RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,lte=4"` + RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,rLevel"` ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"` }{} @@ -599,7 +599,7 @@ func (s *Service) serveManifestEntry( func (s *Service) downloadHandler(logger log.Logger, w http.ResponseWriter, r *http.Request, reference swarm.Address, additionalHeaders http.Header, etag, headersOnly bool, rootCh swarm.Chunk) { headers := struct { Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"` - RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,lte=4"` + RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,rLevel"` FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"` ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"` LookaheadBufferSize *int `map:"Swarm-Lookahead-Buffer-Size"` diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index 6a338fa4a57..8801a4b4f15 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -1300,7 +1300,7 @@ func TestBzzDownloadRedundancyLevel(t *testing.T) { Reasons: []jsonhttp.Reason{ { Field: "swarm-redundancy-level", - Error: fmt.Sprintf("want lte:%d", maxValidLevel), + Error: fmt.Sprintf("want redundancy level to be in the range of %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)), }, }, }, diff --git a/pkg/api/validation.go b/pkg/api/validation.go new file mode 100644 index 00000000000..ca7a7b69d2f --- /dev/null +++ b/pkg/api/validation.go @@ -0,0 +1,30 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package api + +import ( + "fmt" + + "github.com/ethersphere/bee/v2/pkg/file/redundancy" + "github.com/go-playground/validator/v10" +) + +const ( + RedundancyLevelTag = "rLevel" +) + +// setupValidation configures custom validation rules and their custom error messages. +func (s *Service) setupValidation() { + s.validate.RegisterValidation(RedundancyLevelTag, func(fl validator.FieldLevel) bool { + level := redundancy.Level(fl.Field().Uint()) + return level.Validate() + }) + + s.validationCustomErrorMessages = map[string]func(err validator.FieldError) error{ + RedundancyLevelTag: func(err validator.FieldError) error { + return fmt.Errorf("want redundancy level to be in the range of %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)) + }, + } +} diff --git a/pkg/file/redundancy/level.go b/pkg/file/redundancy/level.go index 411da15ec98..c4eeacddc9f 100644 --- a/pkg/file/redundancy/level.go +++ b/pkg/file/redundancy/level.go @@ -29,6 +29,11 @@ const ( PARANOID ) +// Validate validates the redundancy level +func (l Level) Validate() bool { + return l >= NONE && l <= PARANOID +} + // GetParities returns number of parities based on appendix F table 5 func (l Level) GetParities(shards int) int { et, err := l.getErasureTable() From d5857af4ebfa00a6a9fdc397c0eb54756655ecc2 Mon Sep 17 00:00:00 2001 From: akrem-chabchoub Date: Mon, 5 Jan 2026 19:30:04 +0100 Subject: [PATCH 9/9] refactor(validation): rename error message map and improve registration error handling --- pkg/api/api.go | 8 ++++---- pkg/api/bytes_test.go | 2 +- pkg/api/bzz_test.go | 4 ++-- pkg/api/validation.go | 10 +++++++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 68a6e996b2b..e81cab0cf62 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -214,9 +214,9 @@ type Service struct { whitelistedWithdrawalAddress []common.Address - preMapHooks map[string]func(v string) (string, error) - validationCustomErrorMessages map[string]func(err validator.FieldError) error - validate *validator.Validate + preMapHooks map[string]func(v string) (string, error) + customValidationMessages map[string]func(err validator.FieldError) error + validate *validator.Validate redistributionAgent *storageincentives.Agent @@ -712,7 +712,7 @@ func (s *Service) mapStructure(input, output any) func(string, log.Logger, http. val = string(v) } var cause error - if msgFn, ok := s.validationCustomErrorMessages[err.Tag()]; ok { + if msgFn, ok := s.customValidationMessages[err.Tag()]; ok { cause = msgFn(err) } else { cause = fmt.Errorf("want %s:%s", err.Tag(), err.Param()) diff --git a/pkg/api/bytes_test.go b/pkg/api/bytes_test.go index 00271e8df99..56ec6574565 100644 --- a/pkg/api/bytes_test.go +++ b/pkg/api/bytes_test.go @@ -449,7 +449,7 @@ func TestBytesRedundancyLevel(t *testing.T) { Reasons: []jsonhttp.Reason{ { Field: "swarm-redundancy-level", - Error: fmt.Sprintf("want redundancy level to be in the range of %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)), + Error: fmt.Sprintf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)), }, }, }, diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index 8801a4b4f15..418e58a5ff6 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -1224,7 +1224,7 @@ func TestBzzUploadRedundancyLevel(t *testing.T) { Reasons: []jsonhttp.Reason{ { Field: "swarm-redundancy-level", - Error: fmt.Sprintf("want lte:%d", maxValidLevel), + Error: fmt.Sprintf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)), }, }, }, @@ -1300,7 +1300,7 @@ func TestBzzDownloadRedundancyLevel(t *testing.T) { Reasons: []jsonhttp.Reason{ { Field: "swarm-redundancy-level", - Error: fmt.Sprintf("want redundancy level to be in the range of %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)), + Error: fmt.Sprintf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)), }, }, }, diff --git a/pkg/api/validation.go b/pkg/api/validation.go index ca7a7b69d2f..2e1422ff414 100644 --- a/pkg/api/validation.go +++ b/pkg/api/validation.go @@ -17,14 +17,18 @@ const ( // setupValidation configures custom validation rules and their custom error messages. func (s *Service) setupValidation() { - s.validate.RegisterValidation(RedundancyLevelTag, func(fl validator.FieldLevel) bool { + err := s.validate.RegisterValidation(RedundancyLevelTag, func(fl validator.FieldLevel) bool { level := redundancy.Level(fl.Field().Uint()) return level.Validate() }) + if err != nil { + s.logger.Error(err, "failed to register validation") + panic(err) + } - s.validationCustomErrorMessages = map[string]func(err validator.FieldError) error{ + s.customValidationMessages = map[string]func(err validator.FieldError) error{ RedundancyLevelTag: func(err validator.FieldError) error { - return fmt.Errorf("want redundancy level to be in the range of %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)) + return fmt.Errorf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)) }, } }