Skip to content

Classify synchronization failures and carry the server's reason to the client - #21

Merged
adospace merged 4 commits into
mainfrom
feature/sync-error-classification
Oct 19, 2026
Merged

adospace merged 4 commits into
mainfrom
feature/sync-error-classification

Conversation

@adospace

Copy link
Copy Markdown
Owner

Sync failures used to reach apps as a single opaque SynchronizationException, so every app showed "check your internet connection" whether the device was offline, the server was down, or a table was missing on the server. This PR classifies the failure and carries the server's reason to the client. All changes are additive.

What changes

  • Core: SynchronizationException gains Kind (SyncFailureKind), Stage, ErrorCode, TableName, TraceId and ServerMessage, mirrored into Exception.Data["CoreSync.*"]. The message and InnerException are unchanged.
  • Providers (SqlServer, SqlServerCT, PostgreSQL, MySql): the exception they already throw on a failed apply now carries Kind = DataError and the table name. Behaviour is otherwise the same.
  • CoreSync.Http: SyncErrorDetails body, error codes (session-expired, endpoint-not-found, agent-unavailable, agent-error, configuration-error, data-error, server-error) and the X-CoreSync-Trace header.
  • Client: SyncHttpRequestException (still an HttpRequestException) is built from the error headers and body. Against servers that send neither, the client falls back to the status code. Retries stop only on failures the server marks as permanent; unknown codes are retried.
  • Server: an expired upload or download session returns 410 session-expired instead of an unhandled exception. Unexpected failures get the code and trace headers. The new options HandleUnexpectedExceptions and IncludeExceptionDetails default to off, so host exception handling is untouched.
  • MessagePack 3.1.7 → 3.1.12 (GHSA-qhrr-8q5h-9q3h). Patch release; the wire format is unchanged.

Compatibility

  • Old client, new server: headers and body are ignored, and status codes are unchanged except the expired-session case, which was already a failure.
  • New client, old server: classification falls back to the status code. A plain HttpRequestException without a status is Network, one with a status is Unknown.
  • Already in production: RTR Sync Server has been running this through CoreSyncServer since 2026-10-19. Current 1.0.x clients synced against it, including a full initial sync, with zero 5xx.

Tests

210/210 on SQL Server, PostgreSQL, MySQL and SQLite: 178 existing plus 32 new in SyncErrorClassificationTests.

…e client

SyncAgent.SynchronizeAsync still wraps every failure in a SynchronizationException with the same
message and inner exception, but now also reports Kind (Network, Unauthorized, Cancelled,
AnchorTooOld, SessionExpired, ServerUnavailable, DataError, ConfigurationError, ServerError),
Stage, TableName, ErrorCode, TraceId and ServerMessage, mirrored into Exception.Data.

CoreSync.Http.Client rebuilds a SyncHttpRequestException (an HttpRequestException) from the
X-CoreSync-Error / X-CoreSync-Trace headers and the SyncErrorDetails body a server emits,
infers a coarse classification from the status code when it emits neither, and stops retrying
bulk downloads only for failures the server marked as permanent.

CoreSync.Http.Server answers an expired upload/download session with 410 + session-expired
instead of an unhandled InvalidOperationException, and tags unexpected failures with the error
code and trace headers while leaving the host's own exception handling untouched.
…nse handling

- SyncAgent no longer reports every plain HttpRequestException as Network: an older
  CoreSync.Http.Client throws that type from EnsureSuccessStatusCode for a 401/404/500 too, so the
  kind is Network only when the exception carries no status (read through reflection, since
  .NET Standard 2.0 has no HttpRequestException.StatusCode) and Unknown when it does or when the
  runtime cannot tell.
- UnauthorizedAccessException maps to Unauthorized only on remote stages; a local store raising it
  for a read-only database file stays Unknown.
- New agent-error code (not permanent, kind ServerError) for a data store agent that answered a
  request with a failure.
- The HTTP client disposes every response it reads and asks for headers first where it used to rely
  on GetFromJsonAsync; the server registers its OnStarting headers only while the response has not
  started and documents that a bare Kestrel 500 carries none.
3.1.7 is affected by GHSA-qhrr-8q5h-9q3h (nested arrays can exceed buffer length
limits), fixed in 3.1.9. The NuGet audit fails restore on it with warnings as errors.
Patch release: the serialization format is unchanged.
CS0419: HttpResponse.OnStarting has two overloads, and the Release build treats the
warning as an error.
@adospace
adospace merged commit 0ed0c98 into main Oct 19, 2026
2 checks passed
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