Skip to content

[Bug]: Dispatcher swallows controller-invocation TypeError as an empty, unlogged 400 - admin cannot diagnose silently-failed saves (repro: changing the SMTP port) #62650

Description

@docontherock

⚠️ This issue respects the following points: ⚠️

Bug description

When a controller method is invoked with an argument whose type does not match the method signature, lib/private/AppFramework/Http/Dispatcher.php catches the resulting TypeError at the invocation boundary and returns an empty HTTP 400 - without logging anything. From an administrator's point of view the request simply fails with a generic client-side AxiosError: Request failed with status code 400, no server-side log entry exists at any log level, and there is no way to tell a genuinely-handled bad request apart from an internal type mismatch.

This turns any form/controller type-coercion mismatch into a silent, undiagnosable failure in production. The concrete, fully reproducible instance below is saving the mail settings after changing the SMTP port, but the underlying diagnostic gap is general and is the primary point of this report.

This is a recurring class of bug in Nextcloud (see "Prior art" below), which is why the systemic diagnostic issue matters more than any single field fix.

Steps to reproduce

  1. Go to Settings > Administration > Basic settings > Email server.
  2. Change the "Port" field to any value (e.g. 587 → 465).
  3. Trigger save (click outside the field).
  4. Observe: a generic "saving failed" notification.
  5. Verify persistence: occ config:system:get mail_smtpport is unchanged — the save genuinely did not persist (not merely a UI/notification glitch).
  6. Check nextcloud.log: no corresponding error/exception entry, at any log level.
  7. Check the webserver access log: the POST to /settings/admin/mailsettings returns 400 with Content-Length: 0.

Expected behavior

Primary (the point of this report):

  • When the Dispatcher catches a TypeError at the controller-invocation boundary, it should log it (even at debug/info level) with enough context (controller, method, offending parameter/type) so administrators can diagnose an otherwise-silent 400.

Secondary (fix the concrete case):

  • The port field should serialize as a string matching the controller signature (cast back to string before submit, or use a text input with numeric validation instead of type="number"), and/or
  • MailSettingsController::setMailSettings() should accept int|string $mail_smtpport and cast internally, since numeric form fields are prone to exactly this coercion mismatch.

Nextcloud Server version

34

Operating system

Debian/Ubuntu

PHP engine version

PHP 8.4

Web server

Nginx

Database engine version

MariaDB

Is this bug present after an update or on a fresh install?

Fresh Nextcloud Server install

Are you using the Nextcloud Server Encryption module?

None

What user-backends are you using?

  • Default user-backend (database)
  • LDAP/ Active Directory
  • SSO - SAML
  • Other

Configuration report

List of activated Apps

Nextcloud Signing status

Nextcloud Logs

Additional info

Root cause (traced through the code)

  • The port field is bound in the settings frontend as a numeric input:
    <NcTextField v-model="mailConfig.mail_smtpport" type="number" max="65535" min="1" .../>
    Because it is type="number", once edited the bound value becomes a JavaScript number.
  • The save handler POSTs the whole mailConfig object as JSON (axios.post(generateUrl('/settings/admin/mailsettings'), config)), so mail_smtpport is serialized as a JSON number (465) rather than a string ("465").
  • OCA\Settings\Controller\MailSettingsController::setMailSettings() declares the parameter as non-nullable string $mail_smtpport.
  • lib/private/AppFramework/Http/Dispatcher.php has declare(strict_types=1). PHP determines strict-typing from the calling file, so the controller invocation is strictly typed: passing a JSON number where a string is required throws a TypeError at the invocation line.
  • The Dispatcher catches exactly this and returns an empty 400, intentionally not logging it (to distinguish it from TypeErrors thrown inside controller logic):
    } catch (\TypeError $e) {
        // Only intercept TypeErrors occurring on the first line, meaning that the
        // invocation of the controller method failed.
        // Any other TypeError happens inside the controller method logic and should be logged as normal.
        ...
        return new Response(Http::STATUS_BAD_REQUEST);
    }
  • Net effect: setSystemValues() inside setMailSettings() never runs, the new port is silently discarded, and there is zero diagnostic trail — no server log entry, only a generic AxiosError in the browser.

Why the diagnostic gap is the real issue

The specific port mismatch could be fixed in one line on either side (see below). But the reason this took hours to diagnose — and the reason similar reports keep recurring under different symptoms — is that the Dispatcher gives operators nothing to go on. An empty, unlogged 400 at the controller-invocation boundary is indistinguishable, from the outside, from any other bad request, and it is invisible even at the lowest log level (loglevel 0).

Any current or future controller whose signature doesn't match what a form actually serializes will fail this same silent way. Fixing individual fields does not close the class of bug; making the boundary observable does.

Workaround (for other admins hitting this)

Set the value directly via occ, bypassing the web form and its JSON binding entirely:

occ config:system:set mail_smtpport --value=<port>

Metadata

Metadata

Assignees

No one assigned

    Labels

    0. Needs triagePending check for reproducibility or if it fits our roadmap34-feedbackbug

    Type

    Projects

    Status
    To triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions