fix(http): enforce required nil pointers in bodies, not just parameters - #1
Merged
Merged
Conversation
A required pointer field in a request body was silently accepted when
absent. The nil check was guarded by isParamField, so it only ran for
query, path and header parameters — while IsFieldRequired returns true
for a `required:"true"` pointer regardless, so the generated OpenAPI
advertised the field as required. The published contract and the runtime
disagreed, which is worse than either being wrong on its own: clients
trust the spec.
A pointer is how a caller says "I need to tell absent from zero". It is
not a statement about whether the field may be omitted — that is what
required/optional are for.
Also collapses the two empty-string branches into one and confines the
check to value types. For a non-pointer string, {"name":""} and {} are
indistinguishable once decoded, so "" is the only available proxy for
"not supplied" — but a non-nil pointer was demonstrably supplied, so ""
behind one is a real value. Applying the proxy there would have made the
pointer form pointless, and did: a required *string set to "" was
rejected.
Together these make the two forms mean distinct, useful things:
Name string `required:"true"` // must be supplied and non-empty
Name *string `required:"true"` // must be supplied; "" is allowed
Tests cover both pointer cases. The absent case fails when the
isParamField guard is restored.
Not included: dropping omitempty as an optionality marker. It is a
serialization directive doing double duty as a deserialization contract,
and it is the reason optional:"true" goes unnoticed — but removing the
inference flips every such field from optional to required. That is 699
fields in authsome alone, breaking 77 tests across 15 packages, and it
would reject previously-valid client requests across every consumer. It
needs a major version and a codemod, not a patch.
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.
A required pointer field in a request body was silently accepted when absent:
Cause
The nil check in
validateCustomTagswas gated onisParamField, so it only ran for query, path and header parameters. Bodies fell through tocontinue.Meanwhile
IsFieldRequiredreturnstruefor arequired:"true"pointer regardless — the explicit tag is checked before the pointer branch — so the generated OpenAPI advertised the field as required. The published contract and the runtime disagreed, which is worse than either being wrong on its own: clients trust the spec.A pointer is how a caller says "I need to tell absent from zero". It is not a statement about whether the field may be omitted — that is what
required/optionalare for.Change
Drop the
isParamFieldcondition so a nil required pointer is reported wherever it appears.Also collapses the two duplicate empty-string branches into one and confines that check to value types. For a non-pointer string,
{"name":""}and{}are indistinguishable once decoded, so""is the only available proxy for "not supplied" — but a non-nil pointer was demonstrably supplied, so""behind one is a real value. Without this, the first change was self-defeating: a required*stringset to""was rejected, which is exactly the case the pointer form exists to express.The two forms now mean distinct, useful things:
Tests
Both pointer cases are covered.
TestBindRequest_RequiredPointerRejectsAbsentfails when theisParamFieldguard is restored.Full
go test ./...green. Verified downstream against forge and authsome via a localreplace: authsome's suite passes across 79 packages with no changes required.Considered and deliberately not included
Dropping
omitemptyas an optionality marker. It is a serialization directive inencoding/jsondoing double duty as a deserialization contract, and it is the reasonoptional:"true"goes unnoticed — the overloading caused three separate bugs in authsome.But removing the inference flips every such field from optional to required. Measured, not estimated: 699 fields in authsome alone, breaking 77 tests across 15 packages, and it would start rejecting previously-valid client requests across every consumer of this library. That needs a major version and a codemod, not a patch.
Also considered: making
requiredmean present rather than non-empty for value types. Not implementable at this layer — once JSON decodes into a non-pointer string, absent and empty are the same value, so removing the emptiness check deletes required-ness rather than refining it (verified:{}started returning 200). The pointer form above is the answer for that case, which is why it is worth fixing.