mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(apns): retry opposite gateway on BadEnvironmentKeyInToken (v1.4.47.5)
On BadEnvironmentKeyInToken, retry once with the opposite gateway. On success, persist Device.apnsEnvironment correction. Two new unit tests cover retry+persist and no-retry-on-BadDeviceToken.
This commit is contained in:
1 parent
4e1e2bed32
commit
5803612e4d
4 files changed
+128
-9
No files matched your search
@@ -55,6 +55,7 @@ vi.mock("@/lib/db", () => ({
|
||||
device: {
|
||||
findMany: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -489,6 +490,61 @@ describe("sendViaApns — dispatcher fan-out", () => {
|
||||
expect(prisma.device.deleteMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retries the opposite gateway on BadEnvironmentKeyInToken and corrects Device.apnsEnvironment", async () => {
|
||||
// v1.4.47.5 — when Apple returns BadEnvironmentKeyInToken, the
|
||||
// server's gateway choice (driven by Device.apnsEnvironment)
|
||||
// doesn't match the token's actual environment. Try the other
|
||||
// gateway exactly once; if it succeeds, persist the correction.
|
||||
vi.mocked(prisma.device.findMany).mockResolvedValueOnce([
|
||||
{ id: "d1", apnsToken: "tok-a", apnsEnvironment: "production" },
|
||||
] as never);
|
||||
// First send (production gateway): rejected.
|
||||
sendMock.mockResolvedValueOnce({
|
||||
sent: [],
|
||||
failed: [{ device: "tok-a", status: 403, response: { reason: "BadEnvironmentKeyInToken" } }],
|
||||
});
|
||||
// Second send (sandbox gateway): success.
|
||||
sendMock.mockResolvedValueOnce({
|
||||
sent: [{ device: "tok-a" }],
|
||||
failed: [],
|
||||
});
|
||||
|
||||
const r = await sendViaApns("u-1", {
|
||||
title: "t",
|
||||
message: "m",
|
||||
eventType: "MEDICATION_REMINDER",
|
||||
});
|
||||
|
||||
expect(r.ok).toBe(true);
|
||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||
expect(prisma.device.update).toHaveBeenCalledWith({
|
||||
where: { id: "d1" },
|
||||
data: { apnsEnvironment: "sandbox" },
|
||||
});
|
||||
});
|
||||
|
||||
it("does NOT retry the opposite gateway when reason is BadDeviceToken", async () => {
|
||||
vi.mocked(prisma.device.findMany).mockResolvedValueOnce([
|
||||
{ id: "d1", apnsToken: "tok-a", apnsEnvironment: "production" },
|
||||
] as never);
|
||||
sendMock.mockResolvedValueOnce({
|
||||
sent: [],
|
||||
failed: [{ device: "tok-a", status: 410, response: { reason: "BadDeviceToken" } }],
|
||||
});
|
||||
|
||||
const r = await sendViaApns("u-1", {
|
||||
title: "t",
|
||||
message: "m",
|
||||
eventType: "MEDICATION_REMINDER",
|
||||
});
|
||||
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.hardReject).toBe(true);
|
||||
// Only one send attempt — no retry on BadDeviceToken.
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.device.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("strips HTML from the alert body before handing to APNs", async () => {
|
||||
vi.mocked(prisma.device.findMany).mockResolvedValueOnce([
|
||||
{ id: "d1", apnsToken: "tok-a", apnsEnvironment: "sandbox" },
|
||||
|
||||
@@ -562,9 +562,22 @@ export async function sendViaApns(
|
||||
|
||||
for (const device of devices) {
|
||||
if (!device.apnsToken) continue;
|
||||
const env =
|
||||
const initialEnv: "production" | "sandbox" =
|
||||
device.apnsEnvironment === "production" ? "production" : "sandbox";
|
||||
|
||||
// v1.4.47.5 — gateway auto-detect on BadEnvironmentKeyInToken.
|
||||
// The iOS client picks the gateway env when it registers, but in
|
||||
// practice the client's reported env can mismatch the token's
|
||||
// actual environment (e.g. DEBUG build that registered itself as
|
||||
// "production" by accident). Apple returns `BadEnvironmentKeyInToken`
|
||||
// when the provider gateway env doesn't match the token env. Try
|
||||
// the opposite gateway exactly once; if it succeeds, persist the
|
||||
// correction so subsequent sends go straight to the right env.
|
||||
const envSequence: ReadonlyArray<"production" | "sandbox"> =
|
||||
initialEnv === "production"
|
||||
? (["production", "sandbox"] as const)
|
||||
: (["sandbox", "production"] as const);
|
||||
|
||||
// SB-5 v1.4.40 — only MEDICATION_REMINDER opts in to Focus-bypass.
|
||||
// The user explicitly asked the server to nudge them about a dose;
|
||||
// a missed dose is the textbook "time-sensitive" use case Apple
|
||||
@@ -573,9 +586,12 @@ export async function sendViaApns(
|
||||
// Focus modes — including Sleep — silence them as the user expects.
|
||||
const isTimeSensitive = payload.eventType === "MEDICATION_REMINDER";
|
||||
|
||||
const result = await sendApnsPush({
|
||||
let result: Awaited<ReturnType<typeof sendApnsPush>> | null = null;
|
||||
let workingEnv: "production" | "sandbox" | null = null;
|
||||
for (const attemptEnv of envSequence) {
|
||||
result = await sendApnsPush({
|
||||
deviceToken: device.apnsToken,
|
||||
environment: env,
|
||||
environment: attemptEnv,
|
||||
payload: {
|
||||
alert: { title: payload.title, body: stripHtml(payload.message) },
|
||||
data: {
|
||||
@@ -602,18 +618,46 @@ export async function sendViaApns(
|
||||
: {}),
|
||||
},
|
||||
collapseId: payload.eventType,
|
||||
});
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
if (result.ok) {
|
||||
workingEnv = attemptEnv;
|
||||
break;
|
||||
}
|
||||
// Only retry the OTHER gateway on the specific environment-mismatch
|
||||
// reason. Other failure reasons (BadDeviceToken, expired, network
|
||||
// blips) wouldn't be solved by switching gateways.
|
||||
if (result.reason !== "BadEnvironmentKeyInToken") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result?.ok) {
|
||||
anySuccess = true;
|
||||
if (workingEnv && workingEnv !== initialEnv) {
|
||||
// Persist the correction so subsequent sends go straight to the
|
||||
// right gateway. Log a wide-event warning so the operator sees
|
||||
// the iOS client's reported env was wrong.
|
||||
await prisma.device
|
||||
.update({
|
||||
where: { id: device.id },
|
||||
data: { apnsEnvironment: workingEnv },
|
||||
})
|
||||
.catch(() => {
|
||||
/* persist is best-effort; the send already succeeded */
|
||||
});
|
||||
getEvent()?.addWarning(
|
||||
`APNs device.apnsEnvironment corrected ${initialEnv} → ${workingEnv} after BadEnvironmentKeyInToken`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.shouldDisable) {
|
||||
if (result?.shouldDisable) {
|
||||
deadDeviceIds.push(device.id);
|
||||
}
|
||||
lastFailureReason = result.reason;
|
||||
lastFailureStatus = result.status;
|
||||
lastFailureReason = result?.reason;
|
||||
lastFailureStatus = result?.status;
|
||||
}
|
||||
|
||||
if (deadDeviceIds.length > 0) {
|
||||
|
||||
Reference in new issue
Block a user