Skip to content

feat(dav): Add occ dav:show-listeners to allow better insights into DAV stack#61869

Open
DerDreschner wants to merge 1 commit into
masterfrom
feat/add-dav-debug-command
Open

feat(dav): Add occ dav:show-listeners to allow better insights into DAV stack#61869
DerDreschner wants to merge 1 commit into
masterfrom
feat/add-dav-debug-command

Conversation

@DerDreschner

@DerDreschner DerDreschner commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Right now, there is no way to see which listeners are registered for events being fired in Sabre. This makes debugging and analyzing the situation pretty hard (see #61680 for example). I would even say that we're not being able to really master the whole event system right now, based on some input I've gathered.

To change the situation I've used Claude to generate me an occ command to see all listeners for a command, their priority and give some information about the execution order and how it behaves on exceptions and errors.

Example output
root@7a6bd926eee5:/var/www/html# sudo -u www-data php occ dav:show-listeners --help                   
Description:
  Show all Sabre event listeners with their event and priority

Usage:
  dav:show-listeners [options]

Options:
      --uri=URI         DAV request path the server is built for; controls which subtree plugins are loaded (e.g. "calendars/admin", "addressbooks/admin", "files/admin") [default: ""]
      --user=USER       Set up the given user's session and filesystem so that plugins registered lazily during an authenticated request are listed too
      --event=EVENT     Only show listeners whose event name contains this string (case-insensitive)
      --method=METHOD   Show the resolved firing order for the given HTTP method (e.g. "GET"), merging wildcard and method-specific listeners the way Sabre does at emit time

[...]

root@7a6bd926eee5:/var/www/html# sudo -u www-data php occ dav:show-listeners --user=admin --method=GET

beforeMethod:GET — 9 listener(s), executed top to bottom (lowest priority first):
  Always runs first. A listener returning false or throwing stops the request here (see skip rules below).
+---+----------+------------------+-------------------------------------------------------------------------------------+
| # | Priority | Registered on    | Listener                                                                            |
+---+----------+------------------+-------------------------------------------------------------------------------------+
| 1 | 1        | beforeMethod:*   | OCA\DAV\Connector\Sabre\MaintenancePlugin::checkMaintenanceMode                     |
| 2 | 9        | beforeMethod:*   | OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin::handleAnonymousOptions              |
| 3 | 10       | beforeMethod:*   | Sabre\DAV\Auth\Plugin::beforeMethod                                                 |
| 4 | 20       | beforeMethod:*   | OCA\DAV\Connector\Sabre\DavAclPlugin::beforeMethod                                  |
| 5 | 50       | beforeMethod:*   | OCA\DAV\Connector\Sabre\LockPlugin::getLock                                         |
| 6 | 100      | beforeMethod:*   | OCA\DAV\Connector\Sabre\UserIdHeaderPlugin::beforeMethod                            |
| 7 | 100      | beforeMethod:*   | {closure:OCA\DAV\Server::__construct()} (/var/www/html/apps/dav/lib/Server.php:289) |
| 8 | 100      | beforeMethod:GET | {closure:OCA\DAV\Upload\ChunkingV2Plugin::beforeGet}                                |
| 9 | 200      | beforeMethod:*   | OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin::beforeHandler                      |
+---+----------+------------------+-------------------------------------------------------------------------------------+

method:GET — 6 listener(s), executed top to bottom (lowest priority first):
  Runs only if beforeMethod completed and preconditions held; a handler here serves the request.
+---+----------+---------------+-------------------------------------------------------------------+
| # | Priority | Registered on | Listener                                                          |
+---+----------+---------------+-------------------------------------------------------------------+
| 1 | 90       | method:GET    | OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin::httpGet       |
| 2 | 90       | method:GET    | OCA\DAV\DAV\ViewOnlyPlugin::checkViewOnly                         |
| 3 | 90       | method:GET    | Sabre\DAV\Browser\Plugin::httpGetEarly                            |
| 4 | 100      | method:GET    | Sabre\DAV\CorePlugin::httpGet                                     |
| 5 | 100      | method:GET    | {closure:OCA\DAV\Connector\Sabre\ZipFolderPlugin::handleDownload} |
| 6 | 200      | method:GET    | Sabre\DAV\Browser\Plugin::httpGet                                 |
+---+----------+---------------+-------------------------------------------------------------------+

afterMethod:GET — 7 listener(s), executed top to bottom (lowest priority first):
  Runs only after the method was served successfully (skipped on the early-exit paths listed below).
+---+----------+-----------------+------------------------------------------------------------------+
| # | Priority | Registered on   | Listener                                                         |
+---+----------+-----------------+------------------------------------------------------------------+
| 1 | 50       | afterMethod:*   | OCA\DAV\Connector\Sabre\LockPlugin::releaseLock                  |
| 2 | 100      | afterMethod:*   | OCA\DAV\Connector\Sabre\RequestIdHeaderPlugin::afterMethod       |
| 3 | 100      | afterMethod:*   | OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin::afterMethod        |
| 4 | 100      | afterMethod:*   | OCA\DAV\Profiler\ProfilerPlugin::afterMethod                     |
| 5 | 100      | afterMethod:GET | OCA\DAV\Connector\Sabre\FilesPlugin::handleDownloadToken         |
| 6 | 100      | afterMethod:GET | OCA\DAV\Connector\Sabre\FilesPlugin::httpGet                     |
| 7 | 999      | afterMethod:GET | {closure:OCA\DAV\Connector\Sabre\ZipFolderPlugin::afterDownload} |
+---+----------+-----------------+------------------------------------------------------------------+

At most one of the two chains below normally runs per request — several early-exit paths run neither:

afterResponse — 2 listener(s), executed top to bottom (lowest priority first):
  Runs on the success path, after the response has been sent.
+---+----------+---------------+------------------------------------------------------------------------------------------------------------------------------+
| # | Priority | Registered on | Listener                                                                                                                     |
+---+----------+---------------+------------------------------------------------------------------------------------------------------------------------------+
| 1 | 100      | afterResponse | OCA\DAV\Connector\Sabre\PropFindMonitorPlugin::afterResponse                                                                 |
| 2 | 100      | afterResponse | {closure:OCA\DAV\Connector\Sabre\FilesPlugin::initialize()} (/var/www/html/apps/dav/lib/Connector/Sabre/FilesPlugin.php:149) |
+---+----------+---------------+------------------------------------------------------------------------------------------------------------------------------+

exception — 2 listener(s), executed top to bottom (lowest priority first):
  Runs on the failure path, when a listener or handler throws. If an afterResponse listener throws, both chains run.
+---+----------+---------------+-------------------------------------------------------------+
| # | Priority | Registered on | Listener                                                    |
+---+----------+---------------+-------------------------------------------------------------+
| 1 | 10       | exception     | OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin::logException |
| 2 | 100      | exception     | Sabre\DAV\CorePlugin::exception                             |
+---+----------+---------------+-------------------------------------------------------------+

When chains are skipped for this request:
  * a beforeMethod listener returns false -> method / afterMethod / afterResponse are skipped; that listener owns the response, and the exception chain does NOT run.
  * a beforeMethod listener throws        -> method / afterMethod / afterResponse are skipped; the exception chain runs and Sabre sends an error response.
  * conditional GET matches (304)         -> the 304 response is sent directly; method / afterMethod / afterResponse AND the exception chain are all skipped.
  * any other precondition fails          -> PreconditionFailed is thrown, so the exception chain runs and Sabre sends a 412 response.
  * no handler serves the method          -> NotImplemented is thrown, so the exception chain runs.
  * an afterMethod listener returns false -> the response is never sent and afterResponse is skipped; the exception chain does NOT run.

Checklist

AI (if applicable)

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

…DAV stack

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: David Dreschner <david.dreschner@nextcloud.com>
@DerDreschner DerDreschner force-pushed the feat/add-dav-debug-command branch from 7cb10e8 to cc4db88 Compare July 7, 2026 15:36
@AndyScherzinger AndyScherzinger changed the title feat(dav): Add occ dav:show-listeners to allow better insides into DAV stack feat(dav): Add occ dav:show-listeners to allow better insights into DAV stack Jul 7, 2026

@provokateurin provokateurin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice stuff! I skimmed over it a bit.

Comment on lines +102 to +108
* The order is taken directly from $server->listeners($eventName) — the very
* list Sabre\DAV\Server::emit() iterates at request time — rather than
* re-sorted here. This matters because WildcardEmitterTrait::listeners() sorts
* with array_multisort($priorities, SORT_NUMERIC, $callbacks), so equal
* priorities are tie-broken by comparing the callbacks themselves; a plain
* stable priority sort would not reproduce that. The priority is then looked
* up per callback from the raw registration maps for display only.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this comment should be kept, since it was fixed upstream.

Comment on lines +167 to +169
* Collects the raw registrations that apply to a concrete event name: the
* exact listeners plus every wildcard whose (star-stripped) key is a prefix
* of the event name, each annotated with the event name it was registered

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds really strange. Can you please check in the sabre code, that this is actually true? I kind of doubt it, because it would not really work well in reality.

Comment thread apps/dav/appinfo/info.xml
<command>OCA\DAV\Command\RetentionCleanupCommand</command>
<command>OCA\DAV\Command\SendEventReminders</command>
<command>OCA\DAV\Command\SetAbsenceCommand</command>
<command>OCA\DAV\Command\ShowListeners</command>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command should be registered at runtime and only when the debug mode is enabled. It is not intended to be used by admins.

}

try {
$davServer = new Server($this->buildRequest($uri), '/');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately we have like 2 or 3 ways to create this server object. The entry points in apps/dav/appinfo also add some more stuff on top of that.
Besides fixing the root cause of the entry point mess, I don't know how to workaround this situation.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants