feat(protocol): add optional STATUS_ERROR_DETAIL rejection reason (2.21.0)

Today a server rejection sends a bare STATUS_ERROR and the reason only
reaches the server log, so the client cannot say why a transfer was
refused.  Add an optional, bounded server->client error-detail frame:

  - Status gains STATUS_ERROR_DETAIL appended LAST so existing wire
    values are unchanged.
  - send_error_detail(fd, msg) sends STATUS_ERROR_DETAIL followed by the
    existing length-prefixed string primitive, slicing over-long messages
    to MAX_ERROR_DETAIL_BYTES (4096).
  - receive_status() (and the timed/keepalive status readers) always
    consume the detail body and map the status back to STATUS_ERROR,
    capturing the text into a thread-local buffer exposed by
    protocol_last_error(); a bare STATUS_ERROR leaves it cleared.  Every
    existing call site keeps working and the stream cannot desync.
  - Upgrade the daemon module gate / config validation (config.c), the
    final transfer failure (server.c) and receiver-side path/node
    validation (file_receive.c) to send a concrete reason; surface it on
    the client in client_send.c/config.c.
  - Bump PROTOCOL_VERSION to 2.21.0 (CMake VERSION, CHANGELOG, docs) and
    update the pinned config wire golden hash / CLI-version tests.
  - Add tests/test_protocol_error.c covering mapping+capture, the
    over-long bound, bare-error clearing, and thread-locality.
This commit is contained in:
2026-09-13 12:19:46 +02:00
parent f6e8b6ddc4
commit 88aee6ce94
19 changed files with 356 additions and 43 deletions
+25 -5
View File
@@ -48,6 +48,17 @@
Always cast to double when dividing so the output stays fractional. */
#define BYTES_PER_MIB (1024ULL * 1024ULL)
/* Surface a server rejection to the user. When the last status exchange
carried a STATUS_ERROR_DETAIL reason (protocol 2.21.0) it is appended to the
client-side context; a bare STATUS_ERROR still logs the context alone. */
static void log_server_rejection(const char* context) {
const char* detail = protocol_last_error();
if (detail && detail[0] != '\0')
log_message(LOG_LEVEL_ERROR, "%s: %s", context, detail);
else
log_message(LOG_LEVEL_ERROR, "%s", context);
}
/* Forward declaration for progress-reporting thread used in multithreaded send. */
static int progress_thread_fn(void* arg);
@@ -608,8 +619,10 @@ static bool finalize_transfer(Client* client, const Config* config, ArrayList* r
Status per_file;
if (!receive_status(client->file_descriptor, &per_file))
return false;
if (per_file == STATUS_ERROR)
if (per_file == STATUS_ERROR) {
log_server_rejection("Receiver reported a per-file error");
return false;
}
if (per_file == STATUS_OK) {
((SourceFile*)remove_sources->items[i])->skipped = true;
} else if (per_file != STATUS_NEXT) {
@@ -619,7 +632,13 @@ static bool finalize_transfer(Client* client, const Config* config, ArrayList* r
}
}
Status status;
return receive_status(client->file_descriptor, &status) && status == STATUS_OK;
if (!receive_status(client->file_descriptor, &status))
return false;
if (status != STATUS_OK) {
log_server_rejection("Receiver reported transfer failure");
return false;
}
return true;
}
static void pipeline_cancel(PipelineContextSender* context) {
@@ -914,7 +933,7 @@ static bool send_delete_manifest_early(Client* client, ArrayList* manifest,
return false;
}
if (ack != STATUS_OK) {
log_message(LOG_LEVEL_ERROR, "Server failed to delete files before the transfer");
log_server_rejection("Server failed to delete files before the transfer");
return false;
}
return true;
@@ -991,7 +1010,7 @@ static int incremental_check(Client* client, File* file, const Config* config,
if (!receive_status(client->file_descriptor, &s))
return -1;
if (s == STATUS_ERROR) {
log_message(LOG_LEVEL_ERROR, "Server reported error for file");
log_server_rejection("Server reported error for file");
return -1;
}
if (s == STATUS_OK)
@@ -1025,7 +1044,7 @@ static int incremental_check(Client* client, File* file, const Config* config,
return 3;
}
if (s != STATUS_NEXT) {
log_message(LOG_LEVEL_ERROR, "Unexpected server status");
log_server_rejection("Unexpected server status");
send_status(client->file_descriptor, STATUS_ERROR);
return -1;
}
@@ -1119,6 +1138,7 @@ static int send_append(const Client* client, File* file, Config* config,
return rc;
}
if (resp != STATUS_APPEND_OK) {
log_server_rejection("Unexpected append-verify response");
send_status(fd, STATUS_ERROR);
return -1;
}