mongodb/laravel-mongodb · error · InvalidArgumentException

Aggregation nesting exceeds the maximum of

Error message

Aggregation nesting exceeds the maximum of %d levels.

What it means

The aggregate pipeline nesting (recursion through $facet, $lookup and $unionWith sub-pipelines) went deeper than 100 levels, the library's guard mirroring BSON's 100-level nesting limit. The check runs recursively in ensureNoNestedWriteInAggregation, incrementing `$level` for each nested sub-pipeline.

Solutions

  1. Flatten the pipeline: reduce nesting of $facet/$lookup/$unionWith sub-pipelines to under 100 levels.
  2. Restructure recursive lookups as iterative queries or use $graphLookup for recursive traversal instead of nested $lookup stages.
  3. Validate pipeline depth client-side before invoking the tool.

Example fix

// before: 101 nested stages, each [['$lookup' => ['from' => 'x', 'pipeline' => <next>]]]
// after
{"aggregate": "users", "pipeline": [{"$lookup": {"from": "orders", "localField": "_id", "foreignField": "user_id", "as": "orders"}}, {"$limit": 10}]}
Defensive patterns

Strategy: validation

Validate before calling

function pipelineDepth(array $pipeline): int {
    $max = 1;
    foreach ($pipeline as $stage) {
        $name = str_replace('$', '', (string) array_key_first($stage));
        $body = $stage[array_key_first($stage)];
        if ($name === 'facet' && is_array($body)) {
            foreach ($body as $sub) { $max = max($max, 1 + pipelineDepth($sub)); }
        } elseif (in_array($name, ['lookup', 'unionWith'], true) && is_array($body) && isset($body['pipeline'])) {
            $max = max($max, 1 + pipelineDepth($body['pipeline']));
        }
    }
    return $max;
}
if (pipelineDepth($pipeline) > 100) { throw new \InvalidArgumentException('pipeline too deeply nested'); }

Try / catch

try {
    $result = $tool->handle($request);
} catch (\InvalidArgumentException $e) {
    if (str_contains($e->getMessage(), 'nesting exceeds')) {
        // flatten or restructure the pipeline, then retry
    }
}

Prevention

When it happens

Trigger: An aggregate command whose sub-pipelines inside $facet/$lookup/$unionWith recurse or stack more than 100 levels deep, e.g. 101 nested $lookup.pipeline levels or a $facet whose facet pipelines each nest further past the limit.

Common situations: Machine- or LLM-generated pipelines with pathological nesting; programmatically built recursive facet/lookup trees; deeply generated JSON from another tool pasted in as the command.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of mongodb/laravel-mongodb@0634653039 (2026-09-15). Data as JSON: /api/errors/d6fb3f6c4740c886. Report an issue: GitHub.

Appendix: source

Thrown at src/Tools/DatabaseQuery.php:133

        }

        if ($operation === 'aggregate') {
            // Check nested write ops recursively with conservative allow list
            $this->ensureNoNestedWriteInAggregation($command['pipeline'] ?? []);
        }

        return $connection->getDatabase()->command($command)->toArray();
    }

    /**
     * @param array<int, array<string, mixed>> $pipeline
     *
     * @throws InvalidArgumentException
     */
    private function ensureNoNestedWriteInAggregation(array $pipeline, int $level = 1): void
    {
        if ($level > self::MAX_NESTING_LEVEL) {
            throw new InvalidArgumentException(sprintf('Aggregation nesting exceeds the maximum of %d levels.', self::MAX_NESTING_LEVEL));
        }

        if (count($pipeline) > self::MAX_STAGES_PER_PIPELINE) {
            throw new InvalidArgumentException(sprintf('A pipeline exceeds the maximum of %d stages.', self::MAX_STAGES_PER_PIPELINE));
        }

        // These aggregation stages may contain nested aggregation pipelines
        // and must be checked for write ops recursively.
        $supportsNestedWrites = ['facet', 'lookup', 'unionWith'];

        // Every known read-only aggregation stage. Kept as an allow list (rather than
        // denying $merge/$out) so that write stages added in the future are rejected by
        // default. See https://www.mongodb.com/docs/manual/reference/mql/aggregation-stages
        $allowList = array_merge($supportsNestedWrites, [
            'addFields',
            'bucket',
            'bucketAuto',
            'changeStream',

View on GitHub (pinned to 0634653039)