Skip to content

Commit fbdf266

Browse files
fix(gear-sync): surface the backend's actual error in the sync tray (#12)
Gear-sync failures showed a hardcoded message guessed from the HTTP status (e.g. a 404 always claimed 'character not found — link your character on the profile page'), which buried the real reason and is now misleading since the backend auto-provisions characters. ApiResult now carries an optional Detail; the batch gear-sync path extracts FastAPI's 'detail' from the error body and GearSyncService prefers it for the overlay/chat message, falling back to a categorized message when absent. Bump to v0.4.1.
1 parent ca63a9f commit fbdf266

5 files changed

Lines changed: 55 additions & 18 deletions

File tree

XIVRaidPlannerPlugin/Api/ApiResult.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ public readonly struct ApiResult<T>
1717
public T? Value { get; }
1818
public ApiError Error { get; }
1919

20-
private ApiResult(bool ok, T? value, ApiError error)
20+
/// <summary>Server-provided error message (e.g. FastAPI's "detail"), when available.</summary>
21+
public string? Detail { get; }
22+
23+
private ApiResult(bool ok, T? value, ApiError error, string? detail)
2124
{
22-
IsSuccess = ok; Value = value; Error = error;
25+
IsSuccess = ok; Value = value; Error = error; Detail = detail;
2326
}
2427

25-
public static ApiResult<T> Ok(T value) => new(true, value, ApiError.None);
26-
public static ApiResult<T> Fail(ApiError error) => new(false, default, error);
28+
public static ApiResult<T> Ok(T value) => new(true, value, ApiError.None, null);
29+
public static ApiResult<T> Fail(ApiError error, string? detail = null) => new(false, default, error, detail);
2730
}

XIVRaidPlannerPlugin/Api/RaidPlannerClient.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,28 @@ private HttpClient CreateHttpClient()
6767
_ => ApiError.Unknown,
6868
};
6969

70+
/// <summary>Pull the human-readable message out of a FastAPI error body (<c>{"detail": "..."}</c>).</summary>
71+
private static string? ExtractErrorDetail(string? body)
72+
{
73+
if (string.IsNullOrWhiteSpace(body)) return null;
74+
try
75+
{
76+
using var doc = JsonDocument.Parse(body);
77+
if (doc.RootElement.ValueKind == JsonValueKind.Object &&
78+
doc.RootElement.TryGetProperty("detail", out var detail) &&
79+
detail.ValueKind == JsonValueKind.String)
80+
{
81+
var text = detail.GetString();
82+
return string.IsNullOrWhiteSpace(text) ? null : text;
83+
}
84+
}
85+
catch (JsonException)
86+
{
87+
// Non-JSON or unexpected shape — fall back to the categorized error.
88+
}
89+
return null;
90+
}
91+
7092
// ==================== Tier Resolution ====================
7193

7294
/// <summary>
@@ -418,7 +440,8 @@ public async Task<ApiResult<PluginBatchGearsetSyncResult>> SyncBatchGearsetsAsyn
418440
if (!response.IsSuccessStatusCode)
419441
{
420442
_log.Error($"[BatchGearSync] {statusCode} from {endpoint} | body: {responseBody}");
421-
return ApiResult<PluginBatchGearsetSyncResult>.Fail(MapStatus(response.StatusCode));
443+
return ApiResult<PluginBatchGearsetSyncResult>.Fail(
444+
MapStatus(response.StatusCode), ExtractErrorDetail(responseBody));
422445
}
423446

424447
_log.Info($"[BatchGearSync] {statusCode} OK | body: {responseBody}");

XIVRaidPlannerPlugin/Services/GearSyncService.cs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@ private void RecordSyncResult(int jobCount, string? error)
6868
_config.Save();
6969
}
7070

71+
/// <summary>
72+
/// Build the user-facing gear-sync error. Prefers the backend's own message
73+
/// (FastAPI "detail") so the overlay/chat show exactly why it failed, falling
74+
/// back to a categorized message when the server gave no detail.
75+
/// </summary>
76+
private static string BuildSyncErrorMessage(ApiError error, string? detail)
77+
{
78+
if (!string.IsNullOrWhiteSpace(detail))
79+
return $"[XRP] {detail}";
80+
81+
return error switch
82+
{
83+
ApiError.Unauthorized => "[XRP] API key rejected — re-authorize via /xrp config.",
84+
ApiError.NotFound => "[XRP] Gear sync failed (404) — check your API URL in /xrp config.",
85+
ApiError.Server => "[XRP] Backend error during gear sync (500). Check the server logs.",
86+
ApiError.Network => "[XRP] Gear sync failed — network error. Check your API URL and connection.",
87+
_ => "[XRP] Gear sync rejected by server. Check the Dalamud log for details.",
88+
};
89+
}
90+
7191
// ==================== Pure diff (TDD) ====================
7292

7393
/// <summary>
@@ -323,14 +343,7 @@ public void SyncSavedGearsets()
323343
}
324344
else
325345
{
326-
var errMsg = result.Error switch
327-
{
328-
ApiError.Unauthorized => "[XRP] API key rejected — re-authorize via /xrp config.",
329-
ApiError.NotFound => $"[XRP] Gearset sync failed (404): character '{charName}' on '{charWorld}' not found, or API URL is wrong. Link your character on the profile page. Check the Dalamud log for details.",
330-
ApiError.Server => "[XRP] Backend error during gearset sync (500). Check the server logs.",
331-
ApiError.Unknown => $"[XRP] Gearset sync rejected by server (422). Your character may not be linked, or a payload field is invalid. Check the Dalamud log for details.",
332-
_ => "[XRP] Gearset sync failed — network error. Check your API URL and connection.",
333-
};
346+
var errMsg = BuildSyncErrorMessage(result.Error, result.Detail);
334347
RecordSyncResult(0, errMsg.Replace("[XRP] ", string.Empty));
335348
_thread.RunOnUi(() =>
336349
{
@@ -444,9 +457,7 @@ public void SyncProfileGear()
444457
}
445458
else
446459
{
447-
var errMsg = result.Error == ApiError.Unauthorized
448-
? "[XRP] API key rejected — re-authorize via /xrp config"
449-
: "[XRP] Failed to sync gear. Check connection.";
460+
var errMsg = BuildSyncErrorMessage(result.Error, result.Detail);
450461
RecordSyncResult(0, errMsg.Replace("[XRP] ", string.Empty));
451462
_thread.RunOnUi(() =>
452463
{

XIVRaidPlannerPlugin/XIVRaidPlannerPlugin.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
33
<PropertyGroup>
4-
<Version>0.4.0</Version>
4+
<Version>0.4.1</Version>
55
<PackageProjectUrl>https://github.com/aaronbcarlisle/XIVRaidPlannerPlugin</PackageProjectUrl>
66
<PackageLicenseExpression>AGPL-3.0-or-later</PackageLicenseExpression>
77
<IsPackable>false</IsPackable>

XIVRaidPlannerPlugin/XIVRaidPlannerPlugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"Description": "Displays loot priority rankings during savage raids and auto-logs loot drops to your FFXIV Raid Planner static. Syncs gear, saved gearsets, mounts, and collection progress to the web app. Requires an API key from the web app.\n\nCommands:\n/xrp - Toggle BiS gear viewer\n/xrp priority - Toggle priority overlay\n/xrp split - Toggle split-clear assignment overlay\n/xrp sync - Run all enabled sync modules (gearsets, mounts)\n/xrp gearsync - Sync saved gearsets for all tracked jobs\n/xrp syncgear - Sync currently equipped gear\n/xrp mountsync - Sync owned mounts and totem counts\n/xrp config - Open configuration",
66
"ApplicableVersion": "any",
77
"IconUrl": "https://raw.githubusercontent.com/aaronbcarlisle/XIVRaidPlannerPlugin/main/XIVRaidPlannerPlugin/Images/icon.png",
8-
"Changelog": "v0.4.0 — Gear, mount & collection sync\n\nAdded\n- Mount farm sync: /xrp mountsync (also runs as part of /xrp sync) syncs owned mounts and totem counts to your static.\n- Batch gearset sync: /xrp gearsync syncs your saved gearsets for every tracked job in one pass.\n- /xrp syncgear syncs only your currently equipped gear.\n- Character window Gear Sync overlay — an FFXIV-native sync tray with gear, mount, and collection actions right on your Character window.\n- Collection participant sync and a catalog ID resolver (/xrp resolve-ids).\n- Split Clear overlay: /xrp split surfaces in-game run assignments for split-clear nights.\n\nv0.3.3 fixes still apply: BiS viewer Refresh no longer hides the Sync Gear button or wipes the equipped column, and the embedded plugin icon renders in the installer.",
8+
"Changelog": "v0.4.1 — Clearer sync errors\n\nFixed\n- Gear sync now shows the server's actual reason when it fails, right in the Character window sync tray and in chat, instead of a generic message. Pairs with a website fix so first-time sign-ins no longer hit a 'character not linked' error — your character is created automatically on first sync.\n\nv0.4.0 — Gear, mount & collection sync\n\nAdded\n- Mount farm sync: /xrp mountsync (also runs as part of /xrp sync) syncs owned mounts and totem counts to your static.\n- Batch gearset sync: /xrp gearsync syncs your saved gearsets for every tracked job in one pass.\n- /xrp syncgear syncs only your currently equipped gear.\n- Character window Gear Sync overlay — an FFXIV-native sync tray with gear, mount, and collection actions right on your Character window.\n- Collection participant sync and a catalog ID resolver (/xrp resolve-ids).\n- Split Clear overlay: /xrp split surfaces in-game run assignments for split-clear nights.\n\nv0.3.3 fixes still apply: BiS viewer Refresh no longer hides the Sync Gear button or wipes the equipped column, and the embedded plugin icon renders in the installer.",
99
"Tags": [
1010
"raid",
1111
"loot",

0 commit comments

Comments
 (0)