Skip to content

fix(console): write app command loading errors to stderr - #297

Open
printminion-co wants to merge 1 commit into
masterfrom
mk/fix/occ-console-command-load-error-to-stderr
Open

fix(console): write app command loading errors to stderr#297
printminion-co wants to merge 1 commit into
masterfrom
mk/fix/occ-console-command-load-error-to-stderr

Conversation

@printminion-co

@printminion-co printminion-co commented Jul 31, 2026

Copy link
Copy Markdown
  • Resolves: #

Summary

occ <cmd> --output=json can emit a non-JSON error line on stdout ahead of the JSON payload, while still exiting 0.

$ ./occ app:list --output=json
Connection refused
{"enabled":{"cloud_federation_api":"1.17.0", ...},"disabled":{...}}
$ echo $?
0

Anything that pipes occ ... --output=json into a JSON parser breaks, and there is no non-zero exit code to detect it by. In our case this silently broke dev-install.sh: the IONOS setup scripts do occ app:list --output json | jq ..., jq fails to parse, the app list comes back empty, and the scripts then treat every app as disabled.

Cause

In OC\Console\Application::loadCommands(), the catch (\Throwable $e) around loadCommandsFromInfoXml() reports the failure with $output->writeln() — stdout — whereas every other diagnostic in the same method already uses $output->getErrorOutput() (insufficient memory limit, "Nextcloud is not installed", "requires upgrade", maintenance-mode notices). The command itself then runs and succeeds, so the message is pure stdout contamination.

$output is typed ConsoleOutputInterface, so getErrorOutput() is guaranteed by the signature.

How we hit it

notify_push on a setup that has the phpredis extension loaded but no Redis configured. RedisFactory::isAvailable() only checks extension_loaded('redis'), not whether a redis config block exists, so PushRedisFactory::getRedis() takes the Redis branch, falls back to 127.0.0.1:6379 and throws RedisException: Connection refused while the app's console commands are being constructed. Since notify_push declares 5 commands in info.xml, this happened on every occ invocation.

The app-side behaviour is arguably its own issue, but the console bootstrap should not corrupt stdout regardless of which app fails to load.

--no-warnings is not a workaround: it sets VERBOSITY_QUIET, which suppresses the JSON payload too.

TODO

Upstream tracking — this fix is vanilla and IONOS-agnostic, so it was also submitted to
vanilla Nextcloud. Ideally it lands upstream and reaches us via the next rebase, making the
fork-side carry temporary.

Changes

Two call sites (the app_api maintenance-mode block and the regular installed-apps loop) now write to $output->getErrorOutput(). The logger->error() calls are unchanged, so the failure is still logged.

-								$output->writeln('<error>' . $e->getMessage() . '</error>');
+								$output->getErrorOutput()->writeln('<error>' . $e->getMessage() . '</error>');

Verification

  • ./occ app:list --output=json now writes only JSON to stdout; the error appears on stderr and is still recorded in nextcloud.log with its full stack trace.
  • Exit code and command behaviour are otherwise unchanged.

Branched off master (vanilla) and free of IONOS specifics, so it can be submitted upstream to nextcloud/server as-is.

Checklist

  • Code is properly formatted
  • Tests (unit, integration, api and/or acceptance) are included — there is no existing test harness for OC\Console\Application
  • Documentation (manuals or wiki) has been updated or is not required
  • Backports requested where applicable — a backport to our ionos-dev-v33 is a separate step

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@printminion-co
printminion-co force-pushed the mk/fix/occ-console-command-load-error-to-stderr branch 2 times, most recently from 1b5de99 to 281a0cc Compare July 31, 2026 08:14
When an app fails to load its commands from info.xml, the error was
written to stdout, while every other diagnostic in loadCommands() uses
$output->getErrorOutput(). The command itself then runs normally and
exits 0, so the message silently corrupts machine-readable output:

    $ ./occ app:list --output=json
    Connection refused
    {"enabled":{...},"disabled":{...}}
    $ echo $?
    0

Anything piping `occ <cmd> --output=json` into a JSON parser breaks, with
no non-zero exit code to detect it by.

Observed with notify_push on a setup that has the phpredis extension
loaded but no Redis configured: RedisFactory::isAvailable() only checks
whether the extension is loaded, so constructing the app's console
commands ends up calling pconnect() and throws RedisException.

--no-warnings is not a workaround for this, as it sets VERBOSITY_QUIET
and suppresses the payload too.

Route the message to the error output instead. It is still reported via
logger->error() exactly as before.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Misha M.-Kupriyanov <kupriyanov@strato.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant