mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor(withings): route activity sync catch-block through typed classifier
Export `recordWithingsSyncFailure` from sync.ts and migrate the activity-sync catch-block to consume it. The shared helper routes through `classifyError`, which reads `WithingsApiError.classification` directly and falls back to the legacy regex only for un-prototyped errors (pg-boss JSON round-trip). Drops the call-site re-parse of `extractWithingsStatus` + `isWithingsRefreshReauthFailure`, removing one duplicate of the classification taxonomy. The body-status 293 / 601 cases now classify as `persistent` / `transient` respectively instead of the legacy regex default of `transient` for everything outside 100/101/102 + 200-299. BL-P3-2 defence-in-depth — the body-status 403 short-circuit stays inline as a call-site override because 403 is not a documented Withings response code (only the HTTP layer emits it for scope-gated resources), so it has no entry in the shared classifier table.
This commit is contained in:
1 parent
d9fe0e5487
commit
f469749efc
2 files changed
+33
-24
No files matched your search
@@ -48,11 +48,7 @@ import {
|
||||
WithingsApiError,
|
||||
classifyWithingsResponse,
|
||||
} from "./response-classifier";
|
||||
import {
|
||||
extractWithingsStatus,
|
||||
isWithingsRefreshReauthFailure,
|
||||
} from "./sync";
|
||||
import { getValidToken } from "./sync";
|
||||
import { getValidToken, recordWithingsSyncFailure } from "./sync";
|
||||
import {
|
||||
isReauthRequired,
|
||||
parkIntegrationAtReauth,
|
||||
@@ -278,24 +274,37 @@ export async function syncUserActivity(
|
||||
ymd(now),
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const status = extractWithingsStatus(message);
|
||||
// v1.4.26 defence-in-depth — a 403 from a scope-gated endpoint
|
||||
// always means scope-missing or token-revoked. Park at
|
||||
// `error_reauth` rather than `transient` so pg-boss stops
|
||||
// retrying and the 3-strike alert doesn't fire. The early
|
||||
// scope-skip above covers the common case; this catches the
|
||||
// race where Withings revokes scope between OAuth and the next
|
||||
// sync without invalidating the refresh token.
|
||||
const isReauth =
|
||||
isWithingsRefreshReauthFailure(message) || status === "403";
|
||||
await recordSyncFailure({
|
||||
userId,
|
||||
integration: "withings",
|
||||
kind: isReauth ? "reauth_required" : "transient",
|
||||
message,
|
||||
errorCode: status,
|
||||
});
|
||||
// v1.4.43 W7-B3 — typed-classification path. The thrown
|
||||
// `WithingsApiError` from `fetchWithingsActivity` carries the
|
||||
// verdict + `withingsStatus` directly; `recordWithingsSyncFailure`
|
||||
// routes it through `classifyError`, which falls back to the regex
|
||||
// parse for plain `Error` instances (e.g. a pg-boss retry that lost
|
||||
// the prototype during JSON round-trip). Pre-v1.4.43 this branch
|
||||
// re-parsed the message via `extractWithingsStatus` +
|
||||
// `isWithingsRefreshReauthFailure`, duplicating logic the
|
||||
// response-classifier already owns.
|
||||
//
|
||||
// BL-P3-2 defence-in-depth — a Withings 403 on this scope-gated
|
||||
// endpoint always means scope-missing or token-revoked; force the
|
||||
// verdict to `reauth_required` so pg-boss stops retrying and the
|
||||
// 3-strike alert still pages on the unexpected case. The
|
||||
// response-classifier's body-status taxonomy doesn't list 403
|
||||
// (Withings emits 403 only through the HTTP layer for scope-gated
|
||||
// resources), so the override has to live at the call site rather
|
||||
// than in the shared classifier.
|
||||
const withingsStatus =
|
||||
err instanceof WithingsApiError ? err.withingsStatus : undefined;
|
||||
if (withingsStatus === 403) {
|
||||
await recordSyncFailure({
|
||||
userId,
|
||||
integration: "withings",
|
||||
kind: "reauth_required",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
errorCode: "403",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
await recordWithingsSyncFailure(userId, err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ export async function syncUserMeasurements(
|
||||
* `Error` instances (e.g. a pg-boss job retry that lost the prototype
|
||||
* during the JSON round-trip).
|
||||
*/
|
||||
async function recordWithingsSyncFailure(
|
||||
export async function recordWithingsSyncFailure(
|
||||
userId: string,
|
||||
err: unknown,
|
||||
): Promise<void> {
|
||||
|
||||
Reference in new issue
Block a user