Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[13.x] Fix validation stalling for minutes on large arrays - #60908

#60908
Open
mariomka wants to merge 1 commit into
laravel:13.xlaravel/framework:13.xfrom
mariomka:fix-validation-stalling-on-large-arraysmariomka/laravel-framework:fix-validation-stalling-on-large-arraysCopy head branch name to clipboard
Open

[13.x] Fix validation stalling for minutes on large arrays#60908
mariomka wants to merge 1 commit into
laravel:13.xlaravel/framework:13.xfrom
mariomka:fix-validation-stalling-on-large-arraysmariomka/laravel-framework:fix-validation-stalling-on-large-arraysCopy head branch name to clipboard

Conversation

@mariomka

Copy link
Copy Markdown
Contributor

An ordinary request body can stall a request for over a minute. Expanding foo.*.bar rules is quadratic in the number of expanded attributes (array items × wildcard rules), so Validator::make() spends that time building the rule set before a single rule runs. post_max_size and web server body limits never trigger, 8000 items here is only ~1.1 MB.

To reproduce, in php artisan tinker:

$rules = ['items' => ['array']];

foreach (range(1, 17) as $i) {
    $rules["items.*.field{$i}"] = ['nullable', 'string'];
}

foreach ([500, 1000, 2000, 4000, 8000] as $count) {
    $data = ['items' => array_fill(0, $count, ['field1' => 'value'])];

    $start = microtime(true);
    Illuminate\Support\Facades\Validator::make($data, $rules)->passes();
    printf("%5d items: %7.2fs\n", $count, microtime(true) - $start);
}
items before after
500 0.29s 0.06s
1000 0.98s 0.11s
2000 3.80s 0.24s
4000 18.59s 0.47s
8000 85.13s 0.98s

A max rule on the array does not help: 'items' => ['array', 'max:500'] still expands and evaluates every items.* rule before reporting that the array is too large.

ValidationRuleParser::explodeWildcardRules() accumulates into $results by reassigning the return value of mergeRules():

$results = $this->mergeRules($results, $key, $rule);

mergeRulesForAttribute() receives $results by value and writes to it, so copy-on-write clones the whole accumulated rule set once per expanded attribute: 68,000 copies of an array averaging 34,000 entries for 17 rules over 4000 items. Validator::passes() itself is linear; all of the cost is in the expansion.

The fix moves the merge body into a private method that takes $results by reference, and the wildcard loop calls that instead of round-tripping through the by-value mergeRules().

No public or protected signatures change: mergeRules() is untouched and mergeRulesForAttribute() keeps its exact signature and return value, delegating to the new private method. explodeWildcardRules() no longer routes through it, so an override there no longer affects wildcard expansion. This is unavoidable, since the by-value return is the bug itself.

No new test, since the behaviour is unchanged and a timing assertion would be flaky.

@mariomka
mariomka force-pushed the fix-validation-stalling-on-large-arrays branch from 7e53f55 to d00b1ed Compare July 27, 2026 15:47
Comment on lines +235 to +237
* @param array $results
* @param string $attribute
* @param string|array $rules

@shaedrich shaedrich Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can narrow this down a little:

Suggested change
* @param array $results
* @param string $attribute
* @param string|array $rules
* @param array<string, mixed> $results
* @param string $attribute
* @param string|array<string, mixed> $rules

or even

Suggested change
* @param array $results
* @param string $attribute
* @param string|array $rules
* @param array<string, (string|\Illuminate\Contracts\Validation\Rule|\Illuminate\Validation\Rules\Exists|\Illuminate\Validation\Rules\Unique)[]> $results
* @param string $attribute
* @param string|array<string, string|\Illuminate\Validation\Rules\Date|\Illuminate\Validation\Rules\Numeric|\Illuminate\Validation\Rules\StringRule|(\Illuminate\Validation\Rules\Date|\Illuminate\Validation\Rules\Numeric|\Illuminate\Validation\Rules\StringRule|\Closure|\Illuminate\Contracts\Validation\InvokableRule|\Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\Rule|\Illuminate\Validation\Rules\Exists|\Illuminate\Validation\Rules\Unique|Illuminate\Contracts\Validation\CompilableRules)[]> $rules

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I kept the docblock the same as mergeRulesForAttribute() and mergeRules() right above it. If I narrow only this one, it will be inconsistent with the rest of the class.

I think it's better to narrow the whole class in one go in a separate follow-up PR, and keep this one focused just on the performance fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.