Repository navigation
[3.0] Keeps error logging on prepared statements for any error message - #9882
albertlast wants to merge 2 commits into
Conversation
The error log is written only through its prepared statement. When that cannot be prepared, mysqli_stmt_bind_param() was handed false and threw a TypeError; the error is now dropped instead of being sent through the regular query path, which may be what failed and whose own errors would come straight back here. What the statement binds is cleaned first: strings are cut to their column on a character boundary, invalid UTF-8 is replaced and NUL bytes are removed. Without that a long message, or one holding bytes the database rejects, made the insert fail and the error was lost. PostgreSQL with pooling prepares the unnamed statement every time, since any other query may replace it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
|
|
||
| $value = mb_scrub(str_replace("\0", '', (string) $error_array[$key]), 'UTF-8'); | ||
|
|
||
| $error_array[$key] = mb_strcut($value, 0, $limit, 'UTF-8'); |
There was a problem hiding this comment.
We have Utils::entitySubstr that should also do this. Although I like the \0 fix. I don't know if its needed or if Utils::entitySubstr already handles that.
There was a problem hiding this comment.
Utils::entitySubstr() counts graphemes, and these columns are limited in bytes. message, url and backtrace are TEXT, which on MySQL is 65,535 bytes, so 65534 characters of a 3-byte script is about 196 KB and the insert fails. #9873 uses it, and against the current head the long 2-byte, 3-byte and emoji messages were not stored on MySQL. It also returns an empty string for invalid UTF-8, because grapheme_substr() returns false and the cast turns that into '', so the message is lost. Both runs are in #9873 (comment).
mb_strcut() cuts by bytes without splitting a character, which is what a byte-limited column needs, and mb_scrub() keeps the readable part of an invalid string. The NUL bytes are not handled by either entitySubstr() or mb_strcut(), which is why that line is separate.
Description
The error log is written through a prepared statement so that error messages, whatever they hold, never pass through SMF's query parser. This keeps it that way and closes the two gaps that still lost errors.
When the statement cannot be prepared, the error is dropped.
mysqli_stmt_bind_param()was handedfalseand threw aTypeError. The error is not sent throughinsert()instead. Whatever caused the error may have broken that path too, and its own errors would be logged by callingerror_insert()again. #9873 does fall back toinsert(), and in testing that fallback fails on both engines: it passes a single flat row, whichinsert()rejects with "Invalid data structure sent to the database" unless backward compatibility is on.What the statement binds is cleaned first, in a new
DatabaseApi::sanitizeErrorRow():mb_strcut(), so the cut never ends inside a character;mb_scrub();Without that, a message longer than its column, or one holding bytes the database rejects, made the insert fail and the error was lost. A byte-wise
substr()has the same problem: cutting inside a multibyte character leaves invalid UTF-8.PostgreSQL with pooling prepares the unnamed statement every time, as it did before, since any other query may replace it. It also reads
$this->persist;$db_persistis not defined inside the class.Testing
tests/Unit/DatabaseApiTest.phpcovers the clean-up.tests/Integration/ErrorInsertTest.phplogs through the realerror_insert()and reads the row back. It runs outside a transaction, becauseerror_insert()rolls back an open transaction on PostgreSQL, and deletes its own rows. On release-3.0 all four awkward-message cases fail on MySQL; with this change both suites pass on MySQL and PostgreSQL.A failed prepare is not in the suite. Forcing one needs either a server-wide MySQL setting or a clash of statement names on PostgreSQL, and the PHP warning that produces would fail the run under
failOnWarning. It was checked by hand instead, with a script callingerror_insert()with 17 kinds of message (quotes, backslashes, SQL,{db_prefix}and%splaceholders, emoji, newlines, HTML, NUL bytes, invalid UTF-8, empty, and 70 KB cut inside 2-, 3- and 4-byte characters), each under an IPv4, an IPv6 and an invalid IP:The prepare was made to fail with
SET GLOBAL max_prepared_stmt_count = 0on MySQL, and on PostgreSQL by preparing another statement namedsmf_log_errorsfirst. The script checked that the prepare really failed before it ran.Issues References (Fixes|Related|Closes)
🤖 Generated with Claude Code