719-286-0751 [email protected]

Fixing the “TypeError: stream_get_meta_data(): Argument #1 ($stream) must be of type resource, bool given” Error in Magento 2

One of the more disruptive Magento 2 failures on S3 remote storage is a PHP fatal that nobody threw on purpose: TypeError: stream_get_meta_data(): Argument #1 ($stream) must be of type resource, bool given. It surfaces in your logs as PHP Fatal error: Uncaught TypeError ... in .../module-aws-s3/Driver/AwsS3.php, and it does not fail gracefully. A web request dies mid-response, or worse, a long-running queue consumer crashes and your async operations stop draining. We'll cover why PHP 8 turns this into a fatal, how to confirm it's the destructor and not an S3 connectivity issue, and a two-part fix that addresses both the symptom and the cause.

Need help fixing your Magento 2 store?
If this error is impacting live revenue, our senior Magento team can help you debug and deploy a fix safely.
Contact us →

Why Magento 2 throws this error

The crash lives in core: MagentoAwsS3DriverAwsS3, specifically fileClose() and __destruct(). fileClose() calls stream_get_meta_data($resource)['uri'] on a handle that has already been closed elsewhere. A closed stream is false (a bool), and under PHP 8 passing a bool to a parameter typed resource throws a TypeError. That throw happens during object teardown — and a throw escaping a destructor becomes an unrecoverable fatal.

  • PHP 8 promoted type mismatches: passing a closed stream to a function typed resource now throws TypeError instead of warning and returning false like PHP 7 did.
  • TypeError extends Error, not Exception. Both implement Throwable, but a catch (Exception $e) block silently misses every Error subclass.
  • A destructor must never let a throwable escape — PHP can't propagate it normally, so it escalates to a fatal error that kills the process.
  • fileClose() had no is_resource() guard, and the in-memory $this->streams registry could still hold handles closed elsewhere, so the same stream got double-closed at teardown.

1. Confirm PHP 8 and locate the core driver

Start with the artifact, not assumptions. Verify the runtime and that the affected class is present.

php -v && ls -1 vendor/magento/module-aws-s3/Driver/AwsS3.php

You should see PHP 8.x and the path printed. If both check out, the bug is in core Magento's S3 driver — not in any custom module of yours.

2. Reproduce the fatal from the logs

Pull the actual failure so you can prove it's the destructor path and not a network error talking to your-bucket.

grep -R "stream_get_meta_data" var/log/*.log var/report/* 2>/dev/null | head

You should see an uncaught TypeError from stream_get_meta_data() inside module-aws-s3/Driver/AwsS3.php during destruction. If you instead see S3 auth or timeout errors, you're chasing a different problem.

Need help fixing your Magento 2 store?
If this error is impacting live revenue, our senior Magento team can help you debug and deploy a fix safely.
Contact us →

3. Widen the destructor catch to Throwable

The destructor only caught Exception, so the TypeError walked straight past it. Catch the common ancestor instead.

// MagentoAwsS3DriverAwsS3::__destruct()
} catch (Throwable $e) {
    // throwing from a destructor causes a fatal error
    $this->logger->critical($e);
}

Now any failure during teardown — TypeError included — is logged as critical rather than escalated to a fatal. A destructor should never throw, and catching Throwable enforces that.

4. Guard fileClose() with is_resource()

Catching the throwable stops the crash, but you still want to fix the cause. Don't read metadata off a non-resource, and prune stale handles before iterating.

// MagentoAwsS3DriverAwsS3::fileClose()
if (!is_resource($resource)) {
    return false;
}
$resourcePath = stream_get_meta_data($resource)['uri'];
foreach ($this->streams as $path => $stream) {
    if (!is_resource($stream)) {
        unset($this->streams[$path]);
        continue;
    }
    // ... existing match-and-write logic
}

Returning false matches the method's bool return type, and unsetting dead handles prevents the double-close at teardown. stream_get_meta_data() now only ever sees a real resource.

5. Ship it as a composer patch and verify

The change is in vendor/, so deliver it through cweagans/composer-patches and confirm it applies.

composer install 2>&1 | grep -i 'aws-s3|patch'

You should see the patch report as applied with no "Could not apply patch" errors. The fix survives every composer install across your environments.

6. Restart consumers and confirm no fatal

Bring the workers back and watch a real batch run.

bin/magento queue:consumers:list && bin/magento queue:consumers:start async.operations.all --max-messages=200

The consumer should process its batch and exit cleanly, with no "Uncaught TypeError" in the logs. We've seen this most often when remote storage is paired with high-throughput async consumers — the same teardown that's harmless on a single request becomes a process-killer when one worker handles thousands of streams.

When this points to a deeper problem

A single patch closes this hole, but the pattern behind it is worth auditing:

  • The catch (Exception) idiom is everywhere, and across a PHP 8 codebase it silently lets every Error subclass through — a latent bug in any cleanup, shutdown, or finally-style code.
  • Resource registries like $this->streams track handles without an invariant that each is still open, so teardown can act on stale resources.
  • Destructors that perform real I/O do failure-prone work at the worst possible moment for a throw.
  • Long-running consumers magnify the blast radius: one bad teardown takes down the whole worker, not one request.

Grep for catch (Exception and bare catch (Exception inside __destruct() and shutdown handlers, then move each to Throwable. Audit every stream_get_meta_data(), fclose(), fread(), and fwrite() for a missing is_resource() guard. Patching one driver clears the symptom; enforcing "destructors catch Throwable and never throw" is the durable fix.

You now have everything to stop the crash and keep your consumers draining. Apply the catch widening and the resource guard, ship them as a patch, and the fatal that used to kill requests and workers becomes a logged line you can ignore.

Install our webapp on your iPhone! Tap and then Add to homescreen.
Share This