mongodb/laravel-mongodb · error · InvalidArgumentException

Only read aggregation stages are allowed. Found

Error message

Only read aggregation stages are allowed. Found: %s

What it means

A stage in the aggregate pipeline (top-level or nested inside $facet/$lookup/$unionWith) is not on the library's allow list of read-only aggregation stages. The allow list deliberately excludes write stages ($merge, $out) and rejects any unknown/future stage by default, so the tool stays strictly read-only.

Solutions

  1. Remove $merge/$out stages — this tool is read-only; persist results via application code instead.
  2. Fix the stage name spelling and ensure each pipeline element is a single-key document like {"$match": {...}}.
  3. Re-express disallowed stages with allowed equivalents (e.g. $sortByCount is allowed; use $group + $sort).
  4. For stages genuinely missing from the allow list, run the aggregation in your own code with the MongoDB PHP library.

Example fix

// before
{"aggregate": "users", "pipeline": [{"$match": {"active": true}}, {"$out": "active_users"}]}
// after
{"aggregate": "users", "pipeline": [{"$match": {"active": true}}, {"$project": {"name": 1, "email": 1}}]}
Defensive patterns

Strategy: validation

Validate before calling

$readStages = ['facet','lookup','unionWith','addFields','bucket','bucketAuto','changeStream','collStats','count','densify','documents','fill','geoNear','graphLookup','group','indexStats','limit','listLocalSessions','listSampledQueries','listSearchIndexes','listSessions','match','planCacheStats','project','redact','replaceRoot','replaceWith','sample','search','searchMeta','set','setWindowFields','shardedDataDistribution','skip','sort','sortByCount','unwind','unset','vectorSearch'];
foreach ($pipeline as $stage) {
    $name = ltrim((string) array_key_first($stage), '$');
    if (!in_array($name, $readStages, true)) {
        throw new \InvalidArgumentException("Stage not allowed: $name");
    }
}

Type guard

$isReadStage = fn (array $stage): bool => in_array(ltrim((string) array_key_first($stage), '$'), [
    'facet','lookup','unionWith','addFields','match','group','project','sort','limit','skip','unwind','set','unset','count','sample','replaceRoot','replaceWith','bucket','bucketAuto','densify','fill','geoNear','graphLookup','redact','setWindowFields','sortByCount','documents','vectorSearch','search','searchMeta','changeStream','collStats','indexStats','planCacheStats','currentOp','listLocalSessions','listSampledQueries','listSearchIndexes','listSessions','shardedDataDistribution','changeStreamSplitLargeEvent'
], true);

Try / catch

try {
    $result = $tool->handle($request);
} catch (\InvalidArgumentException $e) {
    if (str_starts_with($e->getMessage(), 'Only read aggregation stages are allowed')) {
        // remove/replace the offending stage (e.g. $merge, $out) and retry
    }
}

Prevention

When it happens

Trigger: Including {"$merge": ...} or {"$out": ...} in the pipeline; a misspelled stage (e.g. "$math" instead of "$match", or "$sortByCount" with wrong casing); using an exotic/unsupported stage like "$indexStats" is fine but "$planCacheStats" variants or server-specific stages not on the list will fail; nesting a write stage inside a $facet/$lookup/$unionWith sub-pipeline.

Common situations: Trying to persist aggregation results with $out/$merge through the AI tool; typos in stage names; the first element of a stage object not being the stage operator (malformed stage document); copying stages from newer MongoDB versions that the allow list doesn't know yet.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at src/Tools/DatabaseQuery.php:195

            'set',
            'setWindowFields',
            'shardedDataDistribution',
            'skip',
            'sort',
            'sortByCount',
            'unwind',
            'unset',
            'vectorSearch',
        ]);

        // A pipeline is a list of single-key stage documents, e.g. [['$match' => [...]], ...].
        foreach ($pipeline as $stage) {
            $operator = array_key_first($stage);
            $stageName = str_replace('$', '', (string) $operator);
            $stageBody = $stage[$operator];

            if (! in_array($stageName, $allowList, true)) {
                throw new InvalidArgumentException(sprintf('Only read aggregation stages are allowed. Found: %s', $stageName));
            }

            if (! in_array($stageName, $supportsNestedWrites, true)) {
                continue;
            }

            switch ($stageName) {
                case 'facet':
                    array_walk($stageBody, fn ($pipeline) => $this->ensureNoNestedWriteInAggregation($pipeline, $level + 1));

                    break;

                case 'lookup':
                case 'unionWith':
                    // Both stages take an optional single sub-pipeline; unionWith may also be a plain collection name.
                    if (is_array($stageBody) && isset($stageBody['pipeline'])) {
                        $this->ensureNoNestedWriteInAggregation($stageBody['pipeline'], $level + 1);
                    }

View on GitHub (pinned to 0634653039)