diff --git a/src/CoreSync.Http.Client/Implementation/SyncProviderHttpClient.cs b/src/CoreSync.Http.Client/Implementation/SyncProviderHttpClient.cs
index 2f212fe..9dfe28f 100644
--- a/src/CoreSync.Http.Client/Implementation/SyncProviderHttpClient.cs
+++ b/src/CoreSync.Http.Client/Implementation/SyncProviderHttpClient.cs
@@ -32,6 +32,43 @@ public SyncProviderHttpClient(IHttpClientFactory httpClientProvider, SyncProvide
_options = options;
}
+ ///
+ /// Throws for an unsuccessful response, rebuilding from the
+ /// server's 410 Gone signal so that callers see the same typed error they would from a local provider.
+ ///
+ ///
+ /// Used in place of on every sync endpoint:
+ /// without it an aged-out anchor arrives as a bare and the only
+ /// thing an application can tell the user is that synchronization failed.
+ ///
+ private static async Task EnsureSyncSuccessAsync(HttpResponseMessage response, CancellationToken cancellationToken)
+ {
+ if (response.StatusCode == HttpStatusCode.Gone &&
+ response.Headers.TryGetValues(SyncHttpHeaders.ErrorCode, out var errorCodes) &&
+ errorCodes.Contains(SyncHttpErrorCodes.AnchorTooOld))
+ {
+ SyncAnchorTooOldError? error = null;
+ try
+ {
+ error = await response.Content.ReadFromJsonAsync(cancellationToken);
+ }
+ catch (JsonException)
+ {
+ // The header alone identifies the condition; a body we cannot read must not downgrade
+ // this into an ordinary transport failure.
+ }
+
+ var requestedVersion = error?.RequestedVersion ?? -1;
+ var minValidVersion = error?.MinValidVersion ?? -1;
+
+ throw error?.TableName is { } tableName
+ ? new SyncAnchorTooOldException(tableName, requestedVersion, minValidVersion)
+ : new SyncAnchorTooOldException(requestedVersion, minValidVersion);
+ }
+
+ response.EnsureSuccessStatusCode();
+ }
+
public async Task ApplyChangesAsync([NotNull] SyncChangeSet changeSet, [CanBeNull] Func? onConflictFunc = null, CancellationToken cancellationToken = default)
{
var httpClient = _httpClientFactory.CreateClient(_options.HttpClientName ?? Options.DefaultName);
@@ -48,8 +85,9 @@ public async Task ApplyChangesAsync([NotNull] SyncChangeSet changeSe
SyncProgress?.Invoke(this, new SyncProgressEventArgs(SyncStage.ComputingLocalChanges));
- (await httpClient.PostAsJsonAsync($"/{_options.SyncControllerRoute}/changes-bulk-begin", bulkChangeSet, cancellationToken))
- .EnsureSuccessStatusCode();
+ await EnsureSyncSuccessAsync(
+ await httpClient.PostAsJsonAsync($"/{_options.SyncControllerRoute}/changes-bulk-begin", bulkChangeSet, cancellationToken),
+ cancellationToken);
var listOfItemsToUpload = new List();
@@ -74,8 +112,9 @@ public async Task ApplyChangesAsync([NotNull] SyncChangeSet changeSe
beginUploadItemContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-msgpack");
- (await httpClient.PostAsync($"{_options.SyncControllerRoute}/changes-bulk-item-binary", beginUploadItemContent, cancellationToken))
- .EnsureSuccessStatusCode();
+ await EnsureSyncSuccessAsync(
+ await httpClient.PostAsync($"{_options.SyncControllerRoute}/changes-bulk-item-binary", beginUploadItemContent, cancellationToken),
+ cancellationToken);
}
else
{
@@ -85,8 +124,9 @@ public async Task ApplyChangesAsync([NotNull] SyncChangeSet changeSe
Items = listOfItemsToUpload
}), Encoding.UTF8, "application/json");
- (await httpClient.PostAsync($"{_options.SyncControllerRoute}/changes-bulk-item", beginUploadItemContent, cancellationToken))
- .EnsureSuccessStatusCode();
+ await EnsureSyncSuccessAsync(
+ await httpClient.PostAsync($"{_options.SyncControllerRoute}/changes-bulk-item", beginUploadItemContent, cancellationToken),
+ cancellationToken);
}
SyncProgress?.Invoke(this, new SyncProgressEventArgs(SyncStage.ApplyChanges, skip / (double)changeSet.Items.Count));
@@ -94,7 +134,7 @@ public async Task ApplyChangesAsync([NotNull] SyncChangeSet changeSe
var remoteChangeSetResponse = await httpClient.PostAsync(
$"{_options.SyncControllerRoute}/changes-bulk-complete{(_options.UseBinaryFormat ? "-binary" : string.Empty)}/{sessionId}", null, cancellationToken);
- remoteChangeSetResponse.EnsureSuccessStatusCode();
+ await EnsureSyncSuccessAsync(remoteChangeSetResponse, cancellationToken);
SyncProgress?.Invoke(this, new SyncProgressEventArgs(SyncStage.ApplyChanges, 1.0));
@@ -115,7 +155,7 @@ public async Task GetChangesAsync(Guid otherStoreId, SyncFilterPa
}
var response = await httpClient.SendAsync(request, cancellationToken);
- response.EnsureSuccessStatusCode();
+ await EnsureSyncSuccessAsync(response, cancellationToken);
var bulkSyncChangeSet = await response.Content.ReadFromJsonAsync(cancellationToken)
?? throw new InvalidOperationException();
@@ -181,7 +221,7 @@ private async Task DownloadBulkChangeSetBinary(BulkSyncChangeSet
{
var response = await httpClient.GetAsync($"{_options.SyncControllerRoute}/changes-bulk-item-binary/{bulkSyncChangeSet.SessionId}/{skip}/{_options.BulkItemSize}", cancellationToken);
- response.EnsureSuccessStatusCode();
+ await EnsureSyncSuccessAsync(response, cancellationToken);
using var s = await response.Content.ReadAsStreamAsync(cancellationToken);
var downloadedItems = await CoreSyncMessagePackSerializer.DeserializeAsync