Repository navigation
fix(http): treat an explicit null optional param as omitted - #327
Merged
Merged
Conversation
A null skipped validation and bypassed the declared default, so typed actions threw a TypeError and every endpoint had to restore its defaults by hand. A Nullable validator still receives null, since that is how a route declares null as a value.
ChiragAgg5k
requested review from
Meldiron,
eldadfux and
lohanidamodar
as code owners
September 23, 2026 09:27
Contributor
|
Benchmark resultshttp — Swoole modes (4 cores, 200 VUs, 20s/run)
a = HYPERLOOP_A (process), b = HYPERLOOP_B (coroutine) Shared CI runners — treat absolute numbers as rough, compare modes within a run. Commit 19b1de7. |
A top-level Nullable check missed compositions such as AnyOf containing a Nullable, replacing a meaningful null with the default. Asking the validator whether null is valid covers every composition.
2 tasks done
loks0n
approved these changes
Sep 23, 2026
Closed
2 tasks done
The explicit-null check built and ran the validator before looking at skipValidation, so a skipped validator factory ran, or threw, on requests that used to succeed. Such params now receive null exactly as before.
The regression test pinned whether the validator factory runs. What callers observe is the value the action receives, so assert that instead.
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 2 selected item(s). Reviewed |
A path value wins over a request null for the same key, so building the validator to test null there was wasted work and ran stateful factories an extra time.
Member
Author
|
@greptile review |
Member
Author
|
@greptile review |
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.
Why
An optional param sent as an explicit
nullskips validation and reaches the action asnull, even though the route declared a default. Every action with a typed signature then throws aTypeError, which surfaces as a 500. On Appwrite Cloud,POST /v1/messaging/messages/pushwith"image": nullhit this ~130 times in two weeks (appwrite/appwrite#13849). Appwrite has been patching these one endpoint at a time by making the argument nullable and copying the default into the body ($path ??= '/',$image ??= ''), so each default lives in two places that can drift apart.What
In
Http::getArguments(), an explicitnullfor an optional param is treated exactly as if the param had been omitted, unless the param's validator acceptsnull(isValid(null)):nullis how a route says null is a meaningful value (for example "clear this field"). Those params still receivenull. This coversNullable, compositions such asAnyOfcontaining aNullable, aWhiteListthat listsnull,Wildcard, and validators built by a factory closure.nulldefault are unaffected.skipValidation: trueare unaffected. Their validator is never built or called, and an explicitnullreaches the action as before.Validator factory resolution moved into a private
resolveValidator()that is shared withvalidate().Before / after
Before:
TypeError: ...::action(): Argument #1 ($image) must be of type string, null given, returned as a 500.After: the action runs with
$image = ''and$badge = -1, the same as when both are omitted.Impact on Appwrite (must land before bumping)
Two independent audits covered every
->param()in appwrite/appwrite:nullthere is a 500, so this change is a pure fix.nulland either treat it like the default or crash on it. An example of the crash is site rulebranch, wherenullfails the deployment query with a 500.Seven params give an explicit
nulla meaning of its own. After this change they would receive the default instead:nulltodayPUT /v1/sites/:siteIdproviderRepositoryId''disconnects the repositoryPUT /v1/storage/buckets/:bucketIdmaximumFileSizePUT /v1/storage/buckets/:bucketIdcompressionnonePUT /v1/storage/buckets/:bucketIdencryptiontruePUT /v1/vectorsdb/:databaseId/collections/:collectionIdenabledPOST /v1/userspasswordpassword_personal_datawhen the personal-data policy is onPOST /v1/messaging/messages/emailattachmentsnull[]appwrite/appwrite#13852 wraps each of these validators in
Nullableand keeps its default. That preserves today's behaviour exactly, and the PR has e2e tests showing each one fails without the wrap and passes with it. It must merge together with the bump to the release containing this fix.Cloud should be checked the same way before it picks this up. The first audit flagged that billing address
addressLine2,stateandpostalCodewould store""instead ofnull.Tests
testExplicitNullOptionalParamFallsBackToDefaultcovers these cases:''default is not validated againstWhiteList(['a', 'b']).nullstill receivesnull:Nullablegiven directly or from a factory closure, andAnyOfcontaining aNullable.nulldefault still yieldsnull.skipValidation: trueand a validator factory that throws still receivesnull, and the factory is never called. This fails against the first revision of this PR.The test fails against the previous
Http.php.HttpTestnow saves and restores$_GET/$_POSTaround each test.testCanHookThrowExceptionsleft$_GET['y']set, which leaked into later tests.bin/monorepo test httpandbin/monorepo check http(Pint, PHPStan, Rector) pass.