Repository navigation
configutil: don't panic on a struct-slice field with the wrong YAML shape - #4282
Merged
roman-khimov merged 4 commits intoNov 10, 2026
Merged
Conversation
fabwa
requested review from
End-rey,
carpawell,
cthulhu-rider and
roman-khimov
as code owners
November 7, 2026 10:02
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4282 +/- ##
=======================================
Coverage 32.70% 32.71%
=======================================
Files 673 673
Lines 41698 41701 +3
=======================================
+ Hits 13638 13643 +5
+ Misses 28060 28058 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
End-rey
reviewed
Nov 9, 2026
End-rey
approved these changes
Nov 9, 2026
carpawell
reviewed
Nov 9, 2026
carpawell
approved these changes
Nov 10, 2026
fabwa
force-pushed
the
fix/config-struct-slice-type-assertion
branch
from
November 10, 2026 16:16
4aceaac to
09dcf73
Compare
roman-khimov
approved these changes
Nov 10, 2026
roman-khimov
left a comment
Member
There was a problem hiding this comment.
Next one to get in. Please fix conflicts.
…hape
processStructSlice asserted the raw viper value for a struct-slice
field directly to []any, and each element directly to map[string]any,
both without checking the assertion succeeded. A config field declared
as a slice of structs (e.g. Config.GRPC, a shard list) given the wrong
shape in YAML - a scalar, or a single mapping instead of a list of
mappings - panics with "interface conversion: interface {} is ...,
not []any" instead of being rejected with a clean error.
This is reachable not just at startup (cmd/neofs-node/main.go) but via
SIGHUP config reload on an already-running node
(cmd/neofs-node/config.go's configWatcher/reloadConfig, neither of
which recovers from a panic), turning an operator's config typo into
an unplanned process crash instead of a rejected reload.
processPrimitiveSlice, right below in the same file, already handles
the identical situation safely with the comma-ok form:
`existing, _ := raw.([]any)`. Match that existing pattern instead of
introducing a new error-handling convention: use comma-ok for both
assertions, falling back to the already-present zero values (nil list,
empty item map) when the stored value isn't shaped as expected. This
doesn't silently swallow the malformed config - viper's value is left
untouched, so UnmarshalExact's own mapstructure-based decoding (which
already returns a clean, descriptive error for a type mismatch against
the destination struct) is reached instead of being pre-empted by a
panic.
Signed-off-by: Fabian Wahle <fabian@nash.io>
Review feedback: tolerating a malformed list item by substituting an
empty map meant that an env override on any item of the same list made
processStructSlice rebuild it, and the caller v.Set it back over the
original - so UnmarshalExact never saw the bad value. The config then
loaded with no error at all and a zero-valued entry in its place,
trading the original panic for a silent misconfiguration. The scalar
case had the same hole; its existing test missed it by not setting an
override.
Check the shape up front instead and leave the value alone when it is
wrong, so mapstructure's own type check reports it:
'clients[1]' expected a map or struct, got "string"
Returning nil is already this function's way of saying it has nothing
to override, so no new error plumbing is needed through its call
sites. The pre-scan runs before the loop because item maps are
references into viper's parsed config: bailing out mid-loop would
leave earlier items already mutated in place.
Regression tests cover all four combinations (scalar/malformed item,
with and without an env override); the two override ones fail against
the previous commit.
Signed-off-by: Fabian Wahle <fabian@nash.io>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review feedback. Signed-off-by: Fabian Wahle <fabian@nash.io> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shape of every item is checked before the loop, so the branch was dead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Fabian Wahle <fabian@nash.io>
fabwa
force-pushed
the
fix/config-struct-slice-type-assertion
branch
from
November 10, 2026 19:34
09dcf73 to
7136a21
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
processStructSliceasserted the raw viper value for a struct-slice field directly to[]any, and each element directly tomap[string]any, both without checking the assertion succeeded. A config field declared as a slice of structs (e.g.Config.GRPC, a shard list) given the wrong shape in YAML - a scalar, or a single mapping instead of a list of mappings - panics withinterface conversion: interface {} is ..., not []anyinstead of being rejected with a clean error.This is reachable not just at startup (
cmd/neofs-node/main.go) but via SIGHUP config reload on an already-running node (cmd/neofs-node/config.go'sconfigWatcher/reloadConfig, neither of which recovers from a panic), turning an operator's config typo into an unplanned process crash instead of a rejected reload.Fix
processPrimitiveSlice, right below in the same file, already handles the identical situation safely with the comma-ok form:existing, _ := raw.([]any). This PR matches that existing pattern instead of introducing a new error-handling convention: use comma-ok for both assertions, falling back to the already-present zero values (nil list, empty item map) when the stored value isn't shaped as expected.This doesn't silently swallow the malformed config - viper's value is left untouched, so
UnmarshalExact's own mapstructure-based decoding (which already returns a clean, descriptive error for a type mismatch against the destination struct) is reached instead of being pre-empted by a panic.Test plan
TestEnvUnmarshal_StructSliceAsScalar, confirmingUnmarshalreturns a clean error instead of panicking when a struct-slice field is given a scalar value.go vet ./...clean;go test ./internal/configutil/... ./cmd/neofs-node/...passes.mastertip.🤖 Generated with Claude Code