Log raw response when Garmin returns non-JSON
All checks were successful
Deploy / deploy (push) Successful in 1m40s
All checks were successful
Deploy / deploy (push) Successful in 1m40s
Garmin is returning HTML error pages instead of JSON data. This
change reads the response as text first, checks if it starts with
{ or [, and logs the first 1000 chars of the response body if not.
This will help diagnose what page Garmin is returning (login, captcha,
rate limit, etc).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,17 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
import type { GarminTokens } from "@/types";
|
import type { GarminTokens } from "@/types";
|
||||||
|
|
||||||
|
// Helper to create mock fetch responses with both text() and json() methods
|
||||||
|
function mockJsonResponse(data: unknown, ok = true, status = 200) {
|
||||||
|
const jsonStr = JSON.stringify(data);
|
||||||
|
return {
|
||||||
|
ok,
|
||||||
|
status,
|
||||||
|
text: () => Promise.resolve(jsonStr),
|
||||||
|
json: () => Promise.resolve(data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
import {
|
import {
|
||||||
daysUntilExpiry,
|
daysUntilExpiry,
|
||||||
fetchBodyBattery,
|
fetchBodyBattery,
|
||||||
@@ -245,13 +256,11 @@ describe("fetchHrvStatus", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns Balanced when API returns BALANCED status", async () => {
|
it("returns Balanced when API returns BALANCED status", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(
|
||||||
ok: true,
|
mockJsonResponse({
|
||||||
json: () =>
|
hrvSummary: { lastNightAvg: 45, weeklyAvg: 42, status: "BALANCED" },
|
||||||
Promise.resolve({
|
}),
|
||||||
hrvSummary: { lastNightAvg: 45, weeklyAvg: 42, status: "BALANCED" },
|
);
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
||||||
|
|
||||||
@@ -267,13 +276,11 @@ describe("fetchHrvStatus", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns Unbalanced when API returns UNBALANCED status", async () => {
|
it("returns Unbalanced when API returns UNBALANCED status", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(
|
||||||
ok: true,
|
mockJsonResponse({
|
||||||
json: () =>
|
hrvSummary: { lastNightAvg: 25, weeklyAvg: 42, status: "UNBALANCED" },
|
||||||
Promise.resolve({
|
}),
|
||||||
hrvSummary: { lastNightAvg: 25, weeklyAvg: 42, status: "UNBALANCED" },
|
);
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
||||||
|
|
||||||
@@ -281,10 +288,7 @@ describe("fetchHrvStatus", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns Unknown when API returns no data", async () => {
|
it("returns Unknown when API returns no data", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(mockJsonResponse({}));
|
||||||
ok: true,
|
|
||||||
json: () => Promise.resolve({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
||||||
|
|
||||||
@@ -292,10 +296,9 @@ describe("fetchHrvStatus", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns Unknown when API returns null hrvSummary", async () => {
|
it("returns Unknown when API returns null hrvSummary", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi
|
||||||
ok: true,
|
.fn()
|
||||||
json: () => Promise.resolve({ hrvSummary: null }),
|
.mockResolvedValue(mockJsonResponse({ hrvSummary: null }));
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
||||||
|
|
||||||
@@ -334,16 +337,14 @@ describe("fetchBodyBattery", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns current and yesterday low values on success", async () => {
|
it("returns current and yesterday low values on success", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(
|
||||||
ok: true,
|
mockJsonResponse({
|
||||||
json: () =>
|
bodyBatteryValuesArray: [
|
||||||
Promise.resolve({
|
{ date: "2024-01-15", charged: 85, drained: 60 },
|
||||||
bodyBatteryValuesArray: [
|
],
|
||||||
{ date: "2024-01-15", charged: 85, drained: 60 },
|
bodyBatteryStatList: [{ date: "2024-01-14", min: 25, max: 95 }],
|
||||||
],
|
}),
|
||||||
bodyBatteryStatList: [{ date: "2024-01-14", min: 25, max: 95 }],
|
);
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
||||||
|
|
||||||
@@ -362,14 +363,12 @@ describe("fetchBodyBattery", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns null values when data is missing", async () => {
|
it("returns null values when data is missing", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(
|
||||||
ok: true,
|
mockJsonResponse({
|
||||||
json: () =>
|
bodyBatteryValuesArray: [],
|
||||||
Promise.resolve({
|
bodyBatteryStatList: [],
|
||||||
bodyBatteryValuesArray: [],
|
}),
|
||||||
bodyBatteryStatList: [],
|
);
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
||||||
|
|
||||||
@@ -380,10 +379,7 @@ describe("fetchBodyBattery", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns null values when API returns empty object", async () => {
|
it("returns null values when API returns empty object", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(mockJsonResponse({}));
|
||||||
ok: true,
|
|
||||||
json: () => Promise.resolve({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
||||||
|
|
||||||
@@ -419,14 +415,12 @@ describe("fetchBodyBattery", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles partial data - only current available", async () => {
|
it("handles partial data - only current available", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(
|
||||||
ok: true,
|
mockJsonResponse({
|
||||||
json: () =>
|
bodyBatteryValuesArray: [{ date: "2024-01-15", charged: 70 }],
|
||||||
Promise.resolve({
|
bodyBatteryStatList: [],
|
||||||
bodyBatteryValuesArray: [{ date: "2024-01-15", charged: 70 }],
|
}),
|
||||||
bodyBatteryStatList: [],
|
);
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
||||||
|
|
||||||
@@ -449,16 +443,14 @@ describe("fetchIntensityMinutes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns 7-day intensity minutes total on success", async () => {
|
it("returns 7-day intensity minutes total on success", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(
|
||||||
ok: true,
|
mockJsonResponse({
|
||||||
json: () =>
|
weeklyTotal: {
|
||||||
Promise.resolve({
|
moderateIntensityMinutes: 45,
|
||||||
weeklyTotal: {
|
vigorousIntensityMinutes: 30,
|
||||||
moderateIntensityMinutes: 45,
|
},
|
||||||
vigorousIntensityMinutes: 30,
|
}),
|
||||||
},
|
);
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchIntensityMinutes("test-token");
|
const result = await fetchIntensityMinutes("test-token");
|
||||||
|
|
||||||
@@ -474,10 +466,7 @@ describe("fetchIntensityMinutes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns 0 when no intensity data available", async () => {
|
it("returns 0 when no intensity data available", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(mockJsonResponse({}));
|
||||||
ok: true,
|
|
||||||
json: () => Promise.resolve({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchIntensityMinutes("test-token");
|
const result = await fetchIntensityMinutes("test-token");
|
||||||
|
|
||||||
@@ -485,10 +474,9 @@ describe("fetchIntensityMinutes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns 0 when weeklyTotal is null", async () => {
|
it("returns 0 when weeklyTotal is null", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi
|
||||||
ok: true,
|
.fn()
|
||||||
json: () => Promise.resolve({ weeklyTotal: null }),
|
.mockResolvedValue(mockJsonResponse({ weeklyTotal: null }));
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchIntensityMinutes("test-token");
|
const result = await fetchIntensityMinutes("test-token");
|
||||||
|
|
||||||
@@ -496,16 +484,14 @@ describe("fetchIntensityMinutes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles only moderate intensity minutes", async () => {
|
it("handles only moderate intensity minutes", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(
|
||||||
ok: true,
|
mockJsonResponse({
|
||||||
json: () =>
|
weeklyTotal: {
|
||||||
Promise.resolve({
|
moderateIntensityMinutes: 60,
|
||||||
weeklyTotal: {
|
vigorousIntensityMinutes: 0,
|
||||||
moderateIntensityMinutes: 60,
|
},
|
||||||
vigorousIntensityMinutes: 0,
|
}),
|
||||||
},
|
);
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchIntensityMinutes("test-token");
|
const result = await fetchIntensityMinutes("test-token");
|
||||||
|
|
||||||
@@ -513,16 +499,14 @@ describe("fetchIntensityMinutes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles only vigorous intensity minutes", async () => {
|
it("handles only vigorous intensity minutes", async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue(
|
||||||
ok: true,
|
mockJsonResponse({
|
||||||
json: () =>
|
weeklyTotal: {
|
||||||
Promise.resolve({
|
moderateIntensityMinutes: 0,
|
||||||
weeklyTotal: {
|
vigorousIntensityMinutes: 45,
|
||||||
moderateIntensityMinutes: 0,
|
},
|
||||||
vigorousIntensityMinutes: 45,
|
}),
|
||||||
},
|
);
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await fetchIntensityMinutes("test-token");
|
const result = await fetchIntensityMinutes("test-token");
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,15 @@ export async function fetchHrvStatus(
|
|||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const text = await response.text();
|
||||||
|
if (!text.startsWith("{") && !text.startsWith("[")) {
|
||||||
|
logger.error(
|
||||||
|
{ endpoint: "hrv-service", responseBody: text.slice(0, 1000) },
|
||||||
|
"Garmin returned non-JSON response",
|
||||||
|
);
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
const data = JSON.parse(text);
|
||||||
const status = data?.hrvSummary?.status;
|
const status = data?.hrvSummary?.status;
|
||||||
|
|
||||||
if (status === "BALANCED") {
|
if (status === "BALANCED") {
|
||||||
@@ -120,7 +128,15 @@ export async function fetchBodyBattery(
|
|||||||
return { current: null, yesterdayLow: null };
|
return { current: null, yesterdayLow: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const text = await response.text();
|
||||||
|
if (!text.startsWith("{") && !text.startsWith("[")) {
|
||||||
|
logger.error(
|
||||||
|
{ endpoint: "bodyBattery", responseBody: text.slice(0, 1000) },
|
||||||
|
"Garmin returned non-JSON response",
|
||||||
|
);
|
||||||
|
return { current: null, yesterdayLow: null };
|
||||||
|
}
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
const currentData = data?.bodyBatteryValuesArray?.[0];
|
const currentData = data?.bodyBatteryValuesArray?.[0];
|
||||||
const current = currentData?.charged ?? null;
|
const current = currentData?.charged ?? null;
|
||||||
@@ -170,7 +186,15 @@ export async function fetchIntensityMinutes(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const text = await response.text();
|
||||||
|
if (!text.startsWith("{") && !text.startsWith("[")) {
|
||||||
|
logger.error(
|
||||||
|
{ endpoint: "fitnessstats", responseBody: text.slice(0, 1000) },
|
||||||
|
"Garmin returned non-JSON response",
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const data = JSON.parse(text);
|
||||||
const weeklyTotal = data?.weeklyTotal;
|
const weeklyTotal = data?.weeklyTotal;
|
||||||
|
|
||||||
if (!weeklyTotal) {
|
if (!weeklyTotal) {
|
||||||
|
|||||||
Reference in New Issue
Block a user