Skip to content

getExtOptions() calls $this->errors('clear all'), deleting every error notice in the request #22

Description

@adrianbj

Module version: 2.2.2 (2026-08-23) · ProcessWire: 3.0.271 · PHP: 8.5

Summary

FieldtypeSelectExtOption::getExtOptions() ends with:

$this->errors('clear all');   // line 565

In ProcessWire, all does not mean "all errors belonging to this object" — it means "all errors in the global $notices, regardless of which object added them". Wire::messages(), which errors() delegates to, has two branches (wire/core/Wire/Wire.php, PW 3.0.271):

if(in_array('all', $options)) {
    $notices = $this->wire()->notices;
    $value = $this->wire(new Notices());
    foreach($notices as $notice) {
        if($notice->getName() != $type) continue;
        $value->add($notice);
        if($clear) $notices->remove($notice); // clear global
    }
    if($clear) $this->_notices[$type] = null; // clear local
} else {
    $value = $this->_notices[$type] === null ? $this->wire(new Notices()) : $this->_notices[$type];
    // ...
    if($clear && $value) {
        $this->wire()->notices->removeItems($value); // clear from global notices
    }
}

So every time a SelectExtOption field builds its option list, every error notice raised anywhere in that request is silently deleted. Messages and warnings are unaffected.

Impact

Any admin form containing a SelectExtOption field silently swallows all error notices, including ones raised by unrelated hooks and modules.

In our case a Pages::saveReady hook called $this->wire()->error(...) to tell the editor that a checkbox had been auto-unchecked on save. The notice was added correctly, carried across ProcessPageEdit's post-save redirect correctly by Session::___redirect(), and restored correctly by Session::wakeupNotices() — then removed during the form render, before AdminThemeFramework::renderNotices() ran. The editor saw "Saved Page" and the warnings, but never the error.

Logging the contents of $notices at ready and again at shutdown, on the request following the redirect:

1-READY     notices{ NoticeMessage:Saved Page: /... ~ NoticeError:Self was unchecked because... ~ NoticeWarning:These pronouns... }
3-SHUTDOWN  notices{ NoticeMessage:Saved Page: /... ~ NoticeWarning:These pronouns... ~ NoticeWarning:Website URL - Error 403... }

Only the NoticeError is gone.

Why it is easy to miss

The two early returns above it make the behaviour data-dependent:

if (!$query->rowCount()) return null;                 // never reaches the clear
// ...
if ($query->rowCount() > self::OPTIONSLIMIT) {
    $this->error(...);
    return false;                                     // also never reaches the clear
}
$this->errors('clear all');

Our dev server had an empty source table, so it never reproduced; production had rows, so it reproduced every time. That made it look like an environment difference rather than a module bug.

Note also that the $this->error(...) branch returns immediately, so line 565 is only ever reached when this call did not raise an error. It can only be clearing a stale error from an earlier invocation in the same request.

Suggested fix

$this->errors('clear');

Dropping all takes the second branch, which returns only this object's own errors and calls removeItems() on exactly those — preserving the intent without touching notices raised elsewhere.

Verified on our production install: the missing error notice reappears, and the module's own "Maximum number of selectable options exceeded" error still behaves as before.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions