Diagnose a problem
Work down this page: the two commands first, then the symptom that matches yours. The cases are ordered by how often each has actually bitten.
Before anything else, check that your setup is one Wusel is tested on — Platform support lists which distributions, desktops and Nextcloud versions those are.
First stop: wusel doctor
Before working through the cases below, run:
$ wusel doctor
It gathers, in one pass, what a support case needs and what the sections below
otherwise ask you to collect by hand: the running daemon and its per-thread
kernel wait-channels, the mount table, the FUSE connection’s waiting count, a
timed check that the mountpoint still answers, the effective configuration, and
— from the running daemon over a per-user socket — the engine’s own view of what
each object is doing. A PASS/WARN/FAIL summary sits on top; the headline
check is the one that matters most, and it is the reason doctor sometimes
pauses for two seconds before it prints: it samples the mount twice.
No single instant can tell a wedged mount from a busy one. A request that is
merely in flight has a reply parked for it from the moment it arrives, so
waiting and the parked-reply total agree on any mount that is doing work.
What separates the two is whether the work moves. If the same job — the same
inode, intent and step — is still handed out to a worker two seconds later, or
a reply is still parked with nothing running behind it at all, the mount has
stopped answering and that is a FAIL. If the work moved on, it was load, and
doctor says so. Both samples are printed in the ENGINE section, so the
report shows its own evidence.
To send the result to us, write the bundle to files and attach them:
$ wusel doctor -o wusel-doctor
wrote wusel-doctor.txt and wusel-doctor.json
The report is redacted by default and safe to attach: the engine’s view is
name-free (it speaks in inode numbers, never file names), no directory of the
mount is ever listed, no secret is read (only the server and login, never the
password), and the home path and username are masked in the text. The header
states exactly what is and is not redacted. --no-logs omits the journal tail;
--include-listing turns redaction off for a consenting deep dive.
Every probe is best-effort: one that cannot run says why, and the rest go on — so the report is still useful on a half-broken system, which is exactly when it is needed.
"Is my file saved?" — wusel status
doctor answers is the mount healthy. When the question is what is happening
to my files, use:
$ wusel status
wusel status — account default
UPLOADS (1)
PARKED Documents/report.odt
Permanent (after 3 attempt(s))
DOWNLOADS (1)
caching Videos/talk.mp4 (412.0 MiB)
buffers: 1 open, 1 with unsaved changes
It names files, which is exactly why it is a separate command and not a flag on
doctor: this output is for you, in your terminal, and there is no way to write
it to a file for someone else.
The UPLOADS section is the one to check after saving something. It is read
from the state database rather than from the daemon, so it is reported even when
nothing is mounted, and it distinguishes three states: waiting (owed, the
uploader will get to it), sending (on its way), and PARKED — an upload given
up after a permanent failure such as a conflict, missing permissions or a full
quota. A parked upload is the one case that needs you: the file reads as saved
locally, it is not on the server, and nothing will retry it on its own. Your
bytes are safe in the buffer either way.
DOWNLOADS separates the two things a user would call downloading: reading
is a range being served live to whoever asked for it, caching is a whole-file
hydration running in the background with nobody waiting for it. Individual reads
last milliseconds, so a single run only catches a sample of them —
wusel status --watch redraws once a second.
The desktop integration is two independent channels
Both ride the session (user) D-Bus and both are fail-soft — without them the mount itself keeps working:
- Notifications
-
org.freedesktop.Notifications, shown by the desktop shell. Needs nothing but the user bus. - File-manager status (cloud provider)
-
libcloudproviders— the sidebar entry with idle/syncing/error state in Nautilus. Needs the user bus plus a one-time system-wide registration on every machine:sudo wusel desktop install-provider nautilus -q # restart Nautilus so it re-reads the provider list
On success the daemon logs at startup:
INFO wusel_desktop: registered as a cloud provider (file-manager status) bus_name=…
No notifications, no cloud entry — daemon started over SSH (or cron)
A mount started from an SSH terminal on a desktop machine runs outside the
graphical session’s environment (DBUS_SESSION_BUS_ADDRESS is not set there).
Since systemd distributions share one user bus per user, the daemon falls
back to it automatically ($XDG_RUNTIME_DIR/bus; the log then shows
session bus found via XDG_RUNTIME_DIR) — SSH-started mounts integrate with
the desktop out of the box.
If even that fails, the daemon warns
(no session D-Bus — desktop integration disabled …); the manual override is:
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
wusel mount
Verify the channel end-to-end without mounting:
wusel desktop notify # sends test notifications (info/warning/error)
If the test notifications appear in the graphical session, conflict and error notices from the daemon will too.
Notifications work, but Nautilus shows only a plain mount
The cloud-provider registration is missing on this machine (it is per-machine,
not per-account-config): run the install-provider step from above, restart
Nautilus, and check the startup log for the registered as a cloud provider
line.
ls works, but cat/image viewers fail with Input/output error
Historically two distinct causes; both are visible in the logs now:
-
Transport errors — the
HTTP error:message names the full cause chain ([connect] … → connection reset, TLS, DNS, timeout). -
A server or proxy that ignores
Rangerequests — the daemon warns once:server ignored a Range request and sent the full body …and serves reads correctly anyway. Every partial read then transfers the whole file, so fix the server/proxy config for performance (check for header-stripping proxies, response compression of media types, or server-side encryption).
Everything feels slow
-
Watch the actual traffic:
GETlines (path + byte range) appear at debug level,PROPFINDlikewise. A directory listing is one PROPFIND per level — many GETs mean file content is being read (thumbnailers included). -
Opening a file caches it: the read is served live, and the whole file is hydrated into the LRU cache in the background (emblem online-only → cached). Browsing a folder with a thumbnailer therefore pulls what it reads — bounded by the cache size, not by network. Exclude the mount from indexers if that traffic is unwanted.
-
On a Range-ignoring server (see above) every partial read transfers the whole file — the warning line tells you.
The folder stalls and nothing happens — is it hung, or is it the network?
Almost always the network, and the mount says so on its own: when the server stays unreachable for about ten seconds, a "Connection lost" notification names the server, and the file manager’s account status switches to error. A second notification announces the connection coming back. Both are localized; neither repeats while the outage lasts (one per incident, not one per failed request).
Two things follow from that, when a mount seems stuck:
-
You got the notification — this is an outage, not a defect. Files that are kept offline stay readable; everything else waits. Nothing has to be killed or unmounted, and no work is lost: pending uploads are retried when the connection returns.
-
You got no notification — either the server is answering (then it is not a connectivity problem: look at the journal for the status it answers with), or the notification channel itself is not working. Check that with
wusel desktop notify; if the test notices do not appear either, see No notifications, no cloud entry — daemon started over SSH (or cron).
The journal carries the detail in both cases — the failing URL and the reason
([connect] … dns error, [timeout] …):
journalctl --user -u wusel@default -g 'unreachable|connect|timeout' -n 50
wusel doctor also reports connectivity, and is the better first step if the
mount answers to nothing at all.
Cache vs. server problems
To rule the cache out (or reset a tester’s machine to a fresh-connect state) without touching credentials or config:
wusel cache clear # whole account: metadata, content, pins
wusel cache clear Photos # one subtree, recursively
Run it while the account is not mounted — including for a single path, where
that requirement is a
known limitation rather
than a technical necessity. If a problem survives cache clear, it is not a
caching problem.
What a conflict should look like
An upload that loses against a newer server version keeps both files: the
server version stays, the local edit is saved as
name (conflicted copy <timestamp>).ext next to it. Expect
-
a
WARN … upload conflict — saved a conflicted copylog line, and -
a warning notification on the desktop (localized) — provided the notification channel works (see No notifications, no cloud entry — daemon started over SSH (or cron)).
With text_merge enabled a clean 3-way merge is attempted first; it needs a
cached base version of the file, otherwise it falls back to the conflicted
copy.
A file shows an "uploading" or "sync-error" emblem
Uploads are asynchronous by default: saving a file returns as soon as the change is safe on local disk, and the transfer to the server runs behind it. While it is on its way the file carries an uploading emblem; once it lands the emblem goes back to cached.
A sync-error emblem means the upload was refused for good — most often wrong
permissions on the target, a name conflict, or a full quota (see the
WARN … upload failed permanently log line and the desktop notification). The
change is not lost: the bytes are kept locally, and saving the file again
re-tries the upload once the cause is fixed. Transient problems (the server
briefly down, a dropped connection) are not shown as errors — they retry on
their own until they land.
To make saving wait for the upload and report its result directly — the
pre-async behaviour — set [sync] upload = sync in config.toml.
If it is large files in particular that fail (small ones upload fine), the cause is usually a reverse proxy mishandling the chunked-upload endpoint — see Uploads fail, stall, or spawn conflicted copies.
wusel doctor (see First stop: wusel doctor) lists any objects still owed to
the server and how their uploads stand.
Uploads fail, stall, or spawn conflicted copies
Large files upload with Nextcloud’s chunked-upload protocol: many small PUT`s
to `/remote.php/dav/uploads/, then one assembling MOVE. Almost every upload
problem seen in the field has been a reverse proxy in front of Nextcloud
mishandling that endpoint — not wusel or Nextcloud themselves. Work through the
client switches first (no server access needed); then, if you administer the
server, the proxy checks.
wusel already tolerates the most common proxy misbehaviour on its own: if the
assembling MOVE returns an error (a proxy timeout surfaced as 403/5xx, or a
WAF) but the file did assemble in full, the upload is counted as the success it
is — no sync-error emblem, no conflicted copy. The switches below are for when
uploads still fail or crawl.
Client side (no server access needed)
- Force HTTP/1.1
-
Some proxies drop or truncate HTTP/2 request bodies, so a chunk reaches Nextcloud as 0 bytes — its log then reads
Expected file size of N bytes, but 0 bytes read. HTTP/1.1 avoids this entirely. Inconfig.toml:[tls] http1_only = trueConcurrent reads still parallelise across the connection pool, so the only cost is a few more connections. Try this first — it fixes the widest class of proxy-induced upload failures without touching the server.
- Keep the 3-way merge off
-
[sync] text_mergeis off by default and best left off: it needs a cached base version of the file and can resolve surprisingly. Off, wusel behaves like the reference client — a conflict keeps both versions as a conflicted copy. Confirmconfig.tomlhas notext_merge = true. - Make saving wait for the upload
-
[sync] upload = syncmakes write-back synchronous, so a save blocks until the upload lands and reports its real result at that moment instead of failing quietly in the background. Handy while diagnosing. - Clean up stale conflicted copies
-
repeated failed attempts can leave
name (conflicted copy …)files; they hold nothing the original does not. Deleting them stops them confusing the picture (and, on the server, reclaims the space).
Server side (if you administer the Nextcloud/proxy)
The tell-tale is in Nextcloud’s own log — …/data/nextcloud.log, or on Nextcloud
AIO docker exec nextcloud-aio-nextcloud tail -f /mnt/ncdata/nextcloud.log:
Expected file size of N bytes, but 0 bytes read … and written …
That is the request body being lost in the proxy — the chunk left the client but arrived at Nextcloud empty. It hits every upload client (the official desktop client too), which is how you tell it apart from a wusel bug. Common causes, in the order worth trying — reload the proxy and re-test one large upload after each:
-
Disable HTTP/3 (QUIC). nginx’s HTTP/3 has request-body bugs; comment out
http3 on;and anylisten … quiclines. Clients fall back to HTTP/2. -
Disable TLS 0-RTT.
ssl_early_data on;can lose or duplicate request bodies; comment it out. -
Response and request buffering are different directions — do not confuse them. Keep
proxy_buffering off(the response side: downloads,Range, streaming, WebSocket). Only if the two steps above did not help, turn the request side on:proxy_request_buffering on;. It makes the proxy read each chunk fully before forwarding, so Nextcloud never sees a half-body; with chunked upload the buffered pieces are small. -
No body-size cap, generous timeouts:
client_max_body_size 0;and e.g.proxy_read_timeout 3600s;. -
WAF / ModSecurity: a rule that flags the long
MOVE, or theDELETEof an upload folder, returns 403 — and a blockedDELETEleaves orphaned chunks in/dav/uploads/that slowly eat quota. Exempt/remote.php/dav/uploads/from body inspection.
One 403 you can rule out immediately: the chunked-upload endpoint
/remote.php/dav/uploads/<user>/ requires the account’s real user id, not a
login alias such as an email — unlike /dav/files/, which accepts the alias.
wusel resolves the user id itself (logged at startup as resolved the DAV user
id …), so this is already handled; only if you reproduce with a hand-built URL,
take the id from occ user:list, not the email.
Log levels — and turning debug on in production
The default is INFO: the production narrative (mount/unmount, the startup
cache summary, uploads, push connection, conflicts, warnings, errors) without
per-operation noise. More detail is opt-in via the RUST_LOG environment
variable:
| Level | What it adds |
|---|---|
|
Lifecycle + state-changing events; quiet during pure browsing/reading. |
|
The activity narrative: every WebDAV request (GET/PUT/PROPFIND/…), |
|
Additionally the read-by-read provenance: one line per ~128 KiB kernel read served from the cache blob or the readahead buffer. Very chatty — use for short, targeted sessions. |
Foreground (a shell):
RUST_LOG=debug wusel mount
Production (the systemd user service): set the variable in a drop-in and restart —
systemctl --user edit wusel@default # opens the drop-in editor; add:
# [Service]
# Environment=RUST_LOG=debug
systemctl --user restart wusel@default
journalctl --user -u wusel@default -f # follow the log
Remove the drop-in (or the Environment= line) and restart to return to INFO.
Scoping to one crate
RUST_LOG is a comma-separated list of directives. A bare level is the global
floor for every crate; crate=level overrides one crate. Combine them to raise
detail for one component while keeping the rest quiet — the practical way to use
trace without drowning in read-by-read lines:
RUST_LOG=info,wusel_core=trace wusel mount # everything at info, engine at trace
RUST_LOG=info,wusel_fuse=debug wusel mount # only the FUSE layer verbose
The crate targets are wusel (CLI/daemon), wusel_core (engine: WebDAV, cache,
state, push), wusel_fuse (the mount), wusel_desktop (notifications + file-manager
status).
|
Always keep a global floor. |