Log why the server refused a write, in its own words
A queued sidecar fails to upload with 403 on a credential that pushes the catalog to the same library root in the same pass. `map_status` reduces every non-success to a typed error, which is right for the application and leaves nothing to work from: a read-only share, a file access control rule and a lock all arrive as `PermissionDenied`. Sabre says which in the response body. It is now logged on any failed PUT — the URL, the status, and the first line naming the exception or message, capped at 300 characters because an error page can be a whole document. Only on failure; a success has no body worth reading. Also adds a `put_probe` example that makes the same request from a stored session and prints the reason, for diagnosing this from a desktop rather than from a tablet's logcat. It needs a session on the machine it runs on, which is why the log line above exists as well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -335,7 +335,36 @@ impl RemoteBackend for NextcloudBackend {
|
||||
}
|
||||
|
||||
let resp = req.body(body).send().await.map_err(map_send_error)?;
|
||||
map_status(resp.status(), path.as_str())?;
|
||||
|
||||
// A refused write is the one status whose *body* matters. Sabre names
|
||||
// the exception class and the rule that refused — a read-only share, a
|
||||
// file access control rule, a lock — and `map_status` reduces all of
|
||||
// them to one typed error. That is right for the application and
|
||||
// useless for working out which of them it is, so the reason is logged
|
||||
// before it is discarded.
|
||||
//
|
||||
// Only on failure, and only the first line: a success has no body
|
||||
// worth reading and an error page can be a whole document.
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let url = self.url_for(path);
|
||||
// `text()` consumes the response, which is why this branch returns
|
||||
// rather than falling through to read the headers below.
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
let reason = body
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.find(|l| l.contains("message") || l.contains("exception"))
|
||||
.unwrap_or_else(|| body.trim())
|
||||
.chars()
|
||||
.take(300)
|
||||
.collect::<String>();
|
||||
log::warn!("PUT {url} -> {status}: {reason}");
|
||||
map_status(status, path.as_str())?;
|
||||
// `map_status` returns `Err` for every non-success, so this is
|
||||
// unreachable; stated rather than left to inference.
|
||||
unreachable!("a non-success status always maps to an error");
|
||||
}
|
||||
|
||||
resp.headers()
|
||||
.get(reqwest::header::ETAG)
|
||||
|
||||
Reference in New Issue
Block a user