Hey Jakub,
thanks for the detailed reply.
On 19.11.2025 12:52:35, Jakub Zelenka wrote:
The code one should be probably named StreamErrorCode (to not interact
with what Jordi proposed) and think it will need to be backed as there
should be a straightforward way how to compare it with exception code.
I might need to extend the gen stub as I'm not sure it allows macro
import for enum value like for constants or I will need to come up
with some mapping. I just don't wan to duplicate the numbers. I will
figure something out.
How important is it to have exception code?
I found that for our purposes we generally skip the exception code (i.e.
keep it at zero).
As a simple example, ErrorException does take two integers - one for
severity, and one for code, instead of using the severity as code.
I would propose something similar here: call it StreamError and provide
a public $error property for the actual enum, rather than squeezing it
into $code.
This also solves your gen-stub issues by making them unncessary.
Should StreamException be attached the non-terminal errors which are
caused by the terminal errors? I.e. StreamException clearly is
STREAM_ERROR_CODE_PERMISSION_DENIED for example, but it happens
during a
copy, so, should the information that a STREAM_ERROR_CODE_COPY_FAILED
was caused by that, also be present on the StreamException?
I think I get what you mean from the user point of view but I'm not
sure how could this be reasonably linked.
I could maybe check during stream error if there is EG(exception) it's
a StreamException and if so, I could the update it with this error
(e.g. some property that would hold those error can could be
retrieved). I guess it could be potentially extended for terminal
errors as well so it could have method like getAllErrors which would
return all errors because sometimes there can be more terminal errors
and some of them might get lost (last one will be added). However this
would work just for exception so maybe more generic solution is needed
here (see below).
Replying below to this.
Are stream errors which happen as part of a fully wrapped
functionality
marked as wrapper error as well? Like if the server sent an
invalid TLS
response to a file_get_contents("https://...") operation, making the
whole operation fail? You obviously don't have access to the
intermediary internal stream resource there, where the stream
error is
attached.
I actually thought about this yesterday that it's not ideal that some
errors might get lost in this way so I think it should go to wrapper
errors as well.
If we properly wrap the nested operations into a single error, then it
would be automatically part of the wrapper error, with no need to
explicitly add it to stream error.
Is this what the logged errors section is about? Is that section
talking
about "php_stream_display_wrapper_errors", which can concat some
errors?
I see that this logic is mostly retained.
Yeah the logged errors is an existing mechanism for grouping errors to
a single one (concatenating message). The display is a bit messy
because it separated them by new line or <br/> (if html errors
enabled) but I just kept it to limit BC impact.
I don't think changing the display / grouping here would be problematic
with respect to BC. But I get that this initial draft tries to limit
what it changes.
Do I understand the implementation correctly, that user error
handlers
can be called multiple times for a same operation? E.g. when
copying a
file, you'll get first a PERMISSION_DENIED then a COPY_FAILED? Should
these not be somehow merged? Especially with global user_error
handlers,
these cannot be aware of the fact that the permission denied is
part of
anything else - at least not until possibly a second user_error
invocation happens.
This is somehow related to the exception error grouping as it requires
some sort of linking between the errors.
I guess to make this work we would need some top level wrapping
similar to what logged errors do but instead of concatenating, it
would group the errors in a better way which could then be used in
exception or handler. This would, however, increase the complexity
significantly as it is possible to nest the stream calls (e.g. stream
calls in notification callback or in the new error handler) so there
would probably need to be some stack. Also I'm not sure about the API.
Should we pass always array of errors to the callback (which would
mostly have just one element anyway or somehow chain the StreamError
object (something like previousError()) and what message should be
used in exception (e.g. last error and then helper function to get all
errors)?
I'm willing to look into this but would like to know if this is
something important for user space and what is the usual flow how
could this be used in frameworks and so on. In other words I would
like to get more input to make sure that this is really needed as it
requires quite a bit of effort to do.
Doing this work will definitely tidy up the whole error story. It's not
fundamentally crucial, but it definitely will make for a nicer API.
And yes, chaining StreamError sounds and nesting exceptions to $previous
sounds like the most straightforward way.
Also a possibility for exceptions: this might be a bit of work, but you
could insert a fake frame for exceptions to indicate when an error
appears as a sub-operation. Like:
StreamException: Permission denied
#0 [internal function]: fopen("target.php", "w+")
#1 [internal function]: copy("source.php", "target.php")
#2 ...
I also like the suggestion by Jordi to pass the StreamError right away
into the user handler.
Also, is the internal resource used for e.g. file_get_contents()
leaked
into user error handlers? I'm not sure whether that's desirable. If
that's intended, that's fine too, I think.
Currently yes. I need to actually double check the consequences of
that as I didn't really think about internal resources.
If we decide to go with completely wrapping and calling the user error
handler only once per top-level operation, this point would be moot anyway.
Does stream_get_errors() ever reset? Looking at the code it
doesn't seem
so. It will just forever grow. E.g. fwrite() will report an error
every
single time when the buffer is full and a write is attempted. If
errors
aren't freed, that's just a memory leak (as long as the socket
lives).
This is a serious flaw.
Lastly, stream_get_errors() is overkill in a lot of cases (especially
when using fread()/fwrite() on simple sockets). I just care about the
last error code (there's just going to be one anyway). I'd love
having a
stream_get_last_error() returning just the code.
I guess this makes more sense but it kind of also implies the top
level wrapping as it should contain all errors in the call. Or
alternatively it could be just a ring with limited number of errors be
behave in the same way like openssl_error_string. I guess it depends
if we go with that top level wrapping.
I feel like the top-level wrapping would simplify things a lot (from
point of addressing shortcomings), and if this just works like
json_get_last_error(), where you only ever see the last error, there's
no risks of being unbounded nor having arbitrary size limits.
The only disadvantage of top-level wrapping is probably a bit more
book-keeping as in "storing that there is an outer operation before
invoking an inner operation"? Haven't thought much about the actual
implementation of this, but should be doable.
Thanks,
Bob