fix: prevent panic in uniqueItems validation for unhashable types (#1042)#1045
Conversation
Fixes danielgtaylor#1042 The uniqueItems check in handleArray used map[any]struct{} as a hash set. When array items contain non-hashable types such as map[string]interface{} (produced by malformed JSON like [{"k":"v"}] for a []string field), inserting into the map panics with: http: panic serving ...: hash of unhashable type: map[string]interface {} This caused the server to return no response at all — not even a 500. Fix: introduce a uniqueItemsContains helper that attempts the O(1) map lookup on the fast path. A deferred recover() catches the panic for unhashable types and falls back to an O(n) reflect.DeepEqual linear scan, which: - Never panics regardless of element type - Still detects duplicate unhashable values correctly - Has no impact on the common case (hashable types hit the fast path) Malformed input like [{"k":"v"}] for a []string field now correctly returns a 422 'expected string' validation error instead of crashing. Regression tests added in validate_test.go: - uniqueItems with unhashable object element must not panic - uniqueItems detects duplicate unhashable objects - uniqueItems with mixed hashable and unhashable elements must not panic
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1045 +/- ##
==========================================
+ Coverage 93.14% 93.15% +0.01%
==========================================
Files 23 23
Lines 4901 4955 +54
==========================================
+ Hits 4565 4616 +51
- Misses 272 275 +3
Partials 64 64 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The uniqueItems fix rebuilt a fresh map from all previously-seen items on every element, making the check O(n^2) for every uniqueItems array (not just the unhashable edge case). Since array length is client-controlled, that's a quadratic-blowup vector. Grow a single map incrementally for hashable items (O(1) lookups, the common case) and only fall back to a reflect.DeepEqual scan for genuinely unhashable items (maps/slices), preserving the recover-based guard from danielgtaylor#1042.
|
Thanks for tracking this down, @ArdyJunata! The panic fix is correct and the three regression tests cover the right cases. I pushed a performance follow-up directly to your branch. Benchmarks before and after the changes: ┌───────┬────────────────┬──────────────┬────────────────────┐
│ items │ before this PR │ with this PR │ with the follow-up │
├───────┼────────────────┼──────────────┼────────────────────┤
│ 1,000 │ 0.03 ms │ 8.5 ms │ 0.03 ms │
├───────┼────────────────┼──────────────┼────────────────────┤
│ 4,000 │ 0.10 ms │ 149 ms │ 0.13 ms │
└───────┴────────────────┴──────────────┴────────────────────┘The follow-up keeps your recover approach; it grows a single map incrementally for hashable items (O(1) lookups, the common case) and only falls back to the reflect.DeepEqual scan for genuinely unhashable items. |
Summary
Fixes #1042
Problem
The
uniqueItemscheck inhandleArrayusedmap[any]struct{}as a hash set. When array items contain non-hashable types such asmap[string]interface{}— produced by malformed JSON like[{}]for a[]stringfield — inserting into the map panics:This caused the server to return no response at all instead of a
422 Unprocessable Entity. The panic kills the goroutine serving the request.Reproduction:
Fix
Introduce a
uniqueItemsContainshelper that:map[any]lookup (works for all hashable types — strings, ints, bools, etc.)deferred recover()catches the panic for unhashable types and falls back to an O(n)reflect.DeepEquallinear scanThis means:
[{}]for[]string→ proper422 expected stringerrorTests
Three regression tests added to
validate_test.go:uniqueItems with unhashable object element must not panic[{}]for[]stringreturns422, not a panicuniqueItems detects duplicate unhashable objects[{"a":1},{"a":1}]is still detected as non-uniqueuniqueItems with mixed hashable and unhashable elements["ok", {"k":"v"}]does not panicAll existing tests continue to pass.