Use connectapi.garmin.com directly instead of web proxy
All checks were successful
Deploy / deploy (push) Successful in 1m38s
All checks were successful
Deploy / deploy (push) Successful in 1m38s
The connect.garmin.com/modern/proxy URL returns HTML (website) instead of JSON API responses. Garth library uses connectapi.garmin.com subdomain directly, which is the actual API endpoint. - Change base URL from connect.garmin.com/modern/proxy to connectapi.garmin.com - Update User-Agent to match garth library: GCM-iOS-5.19.1.2 - Factor out headers into getGarminHeaders() to avoid duplication - Remove NK header (not needed when using connectapi subdomain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -173,17 +173,14 @@ describe("fetchGarminData", () => {
|
|||||||
json: () => Promise.resolve(mockResponse),
|
json: () => Promise.resolve(mockResponse),
|
||||||
});
|
});
|
||||||
|
|
||||||
await fetchGarminData("/wellness/daily/123", {
|
await fetchGarminData("/wellness/daily/123", "test-token");
|
||||||
oauth2Token: "test-token",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(global.fetch).toHaveBeenCalledWith(
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
"https://connect.garmin.com/modern/proxy/wellness/daily/123",
|
"https://connectapi.garmin.com/wellness/daily/123",
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer test-token",
|
Authorization: "Bearer test-token",
|
||||||
NK: "NT",
|
"User-Agent": "GCM-iOS-5.19.1.2",
|
||||||
"User-Agent": "GCM-iOS-5.7.2.1",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -210,7 +207,7 @@ describe("fetchGarminData", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
fetchGarminData("/wellness/daily/123", { oauth2Token: "test-token" }),
|
fetchGarminData("/wellness/daily/123", "test-token"),
|
||||||
).rejects.toThrow("Garmin API error: 401");
|
).rejects.toThrow("Garmin API error: 401");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,7 +218,7 @@ describe("fetchGarminData", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
fetchGarminData("/wellness/daily/123", { oauth2Token: "test-token" }),
|
fetchGarminData("/wellness/daily/123", "test-token"),
|
||||||
).rejects.toThrow("Garmin API error: 403");
|
).rejects.toThrow("Garmin API error: 403");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,7 +229,7 @@ describe("fetchGarminData", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
fetchGarminData("/wellness/daily/123", { oauth2Token: "test-token" }),
|
fetchGarminData("/wellness/daily/123", "test-token"),
|
||||||
).rejects.toThrow("Garmin API error: 500");
|
).rejects.toThrow("Garmin API error: 500");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,7 +237,7 @@ describe("fetchGarminData", () => {
|
|||||||
global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
|
global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
fetchGarminData("/wellness/daily/123", { oauth2Token: "test-token" }),
|
fetchGarminData("/wellness/daily/123", "test-token"),
|
||||||
).rejects.toThrow("Network error");
|
).rejects.toThrow("Network error");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -267,7 +264,7 @@ describe("fetchHrvStatus", () => {
|
|||||||
|
|
||||||
expect(result).toBe("Balanced");
|
expect(result).toBe("Balanced");
|
||||||
expect(global.fetch).toHaveBeenCalledWith(
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
"https://connect.garmin.com/modern/proxy/hrv-service/hrv/2024-01-15",
|
"https://connectapi.garmin.com/hrv-service/hrv/2024-01-15",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
headers: expect.objectContaining({
|
headers: expect.objectContaining({
|
||||||
Authorization: "Bearer test-token",
|
Authorization: "Bearer test-token",
|
||||||
@@ -354,7 +351,7 @@ describe("fetchBodyBattery", () => {
|
|||||||
yesterdayLow: 25,
|
yesterdayLow: 25,
|
||||||
});
|
});
|
||||||
expect(global.fetch).toHaveBeenCalledWith(
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
"https://connect.garmin.com/modern/proxy/usersummary-service/stats/bodyBattery/dates/2024-01-15",
|
"https://connectapi.garmin.com/usersummary-service/stats/bodyBattery/dates/2024-01-15",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
headers: expect.objectContaining({
|
headers: expect.objectContaining({
|
||||||
Authorization: "Bearer test-token",
|
Authorization: "Bearer test-token",
|
||||||
@@ -457,7 +454,7 @@ describe("fetchIntensityMinutes", () => {
|
|||||||
|
|
||||||
expect(result).toBe(75);
|
expect(result).toBe(75);
|
||||||
expect(global.fetch).toHaveBeenCalledWith(
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
"https://connect.garmin.com/modern/proxy/fitnessstats-service/activity",
|
"https://connectapi.garmin.com/fitnessstats-service/activity",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
headers: expect.objectContaining({
|
headers: expect.objectContaining({
|
||||||
Authorization: "Bearer test-token",
|
Authorization: "Bearer test-token",
|
||||||
|
|||||||
@@ -4,10 +4,15 @@
|
|||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import type { GarminTokens, HrvStatus } from "@/types";
|
import type { GarminTokens, HrvStatus } from "@/types";
|
||||||
|
|
||||||
const GARMIN_BASE_URL = "https://connect.garmin.com/modern/proxy";
|
// Use connectapi subdomain directly (same as garth library)
|
||||||
|
const GARMIN_API_URL = "https://connectapi.garmin.com";
|
||||||
|
|
||||||
interface GarminApiOptions {
|
// Headers matching garth library's http.py USER_AGENT
|
||||||
oauth2Token: string;
|
function getGarminHeaders(oauth2Token: string): Record<string, string> {
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${oauth2Token}`,
|
||||||
|
"User-Agent": "GCM-iOS-5.19.1.2",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BodyBatteryData {
|
export interface BodyBatteryData {
|
||||||
@@ -17,14 +22,10 @@ export interface BodyBatteryData {
|
|||||||
|
|
||||||
export async function fetchGarminData(
|
export async function fetchGarminData(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
options: GarminApiOptions,
|
oauth2Token: string,
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
const response = await fetch(`${GARMIN_BASE_URL}${endpoint}`, {
|
const response = await fetch(`${GARMIN_API_URL}${endpoint}`, {
|
||||||
headers: {
|
headers: getGarminHeaders(oauth2Token),
|
||||||
Authorization: `Bearer ${options.oauth2Token}`,
|
|
||||||
NK: "NT",
|
|
||||||
"User-Agent": "GCM-iOS-5.7.2.1",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -58,12 +59,8 @@ export async function fetchHrvStatus(
|
|||||||
oauth2Token: string,
|
oauth2Token: string,
|
||||||
): Promise<HrvStatus> {
|
): Promise<HrvStatus> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${GARMIN_BASE_URL}/hrv-service/hrv/${date}`, {
|
const response = await fetch(`${GARMIN_API_URL}/hrv-service/hrv/${date}`, {
|
||||||
headers: {
|
headers: getGarminHeaders(oauth2Token),
|
||||||
Authorization: `Bearer ${oauth2Token}`,
|
|
||||||
NK: "NT",
|
|
||||||
"User-Agent": "GCM-iOS-5.7.2.1",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -113,13 +110,9 @@ export async function fetchBodyBattery(
|
|||||||
): Promise<BodyBatteryData> {
|
): Promise<BodyBatteryData> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GARMIN_BASE_URL}/usersummary-service/stats/bodyBattery/dates/${date}`,
|
`${GARMIN_API_URL}/usersummary-service/stats/bodyBattery/dates/${date}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: getGarminHeaders(oauth2Token),
|
||||||
Authorization: `Bearer ${oauth2Token}`,
|
|
||||||
NK: "NT",
|
|
||||||
"User-Agent": "GCM-iOS-5.7.2.1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -172,13 +165,9 @@ export async function fetchIntensityMinutes(
|
|||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GARMIN_BASE_URL}/fitnessstats-service/activity`,
|
`${GARMIN_API_URL}/fitnessstats-service/activity`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: getGarminHeaders(oauth2Token),
|
||||||
Authorization: `Bearer ${oauth2Token}`,
|
|
||||||
NK: "NT",
|
|
||||||
"User-Agent": "GCM-iOS-5.7.2.1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user