Skip to content

PHP 8.6.0RC3: fclose returns FALSE instead of TRUE with stream wrappers #24256

Description

@thekid

Description

The following code:

<?php

class Buffer {
  public function __construct(public string $bytes= '') { }

  public function uri(): string { return Buffers::uri($this); }
}

class Buffers {
  private static $buffers= [];
  private $id;
  public $context;

  public static function uri(Buffer $buffer): string {
    static $id= 0;
    self::$buffers[$id++]= $buffer;
    return "buffers://{$id}";
  }

  public function stream_open($path, $mode, $options, &$opened) {
    $this->id= (int)parse_url($path, PHP_URL_PATH);
    if (!isset(self::$buffers[$this->id])) {
      throw new Exception("Cannot open buffer {$this->id}");
    }

    $opened= $path;
    return true;
  }

  public function stream_write($bytes) {
    self::$buffers[$this->id]->bytes.= $bytes;
    return strlen($bytes);
  }

  public function stream_close() {
    unset(self::$buffers[$this->id]);
    return true;
  }
}

stream_wrapper_register('buffers', Buffers::class);

$buffer= new Buffer();

$fd= fopen($buffer->uri(), 'rb');
echo 'fd: '; var_dump($fd);
echo 'fwrite: '; var_dump(fwrite($fd, 'Test'));
echo 'fclose: '; var_dump(fclose($fd));

var_dump($buffer);

Resulted in this output:

fd: resource(6) of type (stream)
fwrite: int(4)
fclose: bool(false)
object(Buffer)#1 (1) {
  ["bytes"]=>
  string(4) "Test"
}

But I expected this output instead:

fd: resource(6) of type (stream)
fwrite: int(4)
fclose: bool(true)
object(Buffer)#1 (1) {
  ["bytes"]=>
  string(4) "Test"
}

...or easier to read as a diff:

--- 8.6beta     2026-10-11 11:15:44.899534100 +0200
+++ 8.6rc3      2026-10-11 11:15:31.824666000 +0200
@@ -1,6 +1,6 @@
 fd: resource(6) of type (stream)
 fwrite: int(4)
-fclose: bool(true)
+fclose: bool(false)
 object(Buffer)#1 (1) {
   ["bytes"]=>
   string(4) "Test"

PHP Version

PHP 8.6.0RC3 (cli) (built: Oct  6 2026 17:21:26) (NTS Visual C++ 2026 x64)
Copyright © The PHP Group and Contributors
Built by The PHP Group
Zend Engine v4.6.0RC3, Copyright © Zend by Perforce
    with Zend OPcache v8.6.0RC3, Copyright ©, by Zend by Perforce

Operating System

Windows 11

Activity

  1. thekid commented on Oct 11, 2026

    @thekid
    ContributorAuthor

    Looking at the implementation of fclose(), it returns FALSE when:

    • The stream has the PHP_STREAM_FLAG_NO_FCLOSE bit set (including raising an E_WARNING)
    • php_stream_free() returns non-zero

    As no errors or warnings are raised, it might be the last branch we're running into here.

  2. thekid commented on Oct 11, 2026

    @thekid
    ContributorAuthor

    Digging deeper shows php_stream_free() returns -1, which is the result of php_stream_flush_ex() when closing succeeds. This function is implemented as follows:

    static int php_stream_flush_ex(php_stream *stream, bool closing)
    {
    	int ret = 0;
    
    	if (stream->writefilters.head && stream->ops->write) {
    		php_stream_write_filtered(stream, NULL, 0, closing ? PSFS_FLAG_FLUSH_CLOSE : PSFS_FLAG_FLUSH_INC );
    	}
    
    	stream->flags &= ~PHP_STREAM_FLAG_WAS_WRITTEN;
    
    	if (stream->ops->flush) {
    		ret = stream->ops->flush(stream);
    	}
    
    	return ret;
    }

    ...so I would expect it to return 0, as the above stream wrapper has no flush implementation.


    By adding the following to the Buffers class:

      public function stream_flush() {
        return true;
      }

    ...everything works as expected.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions