From 44944c1e823302431dcc4b3ac8b2bfecdd3f459f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 1 May 2026 09:23:57 +0200 Subject: [PATCH] Use form-encoded bodies for Wahoo OAuth token requests Wahoo's /oauth/token endpoint returns 400 for JSON bodies. OAuth 2.0 requires application/x-www-form-urlencoded for token requests; switch exchangeCode and refreshToken to URLSearchParams. Also include the response body in the thrown error so future failures are diagnosable without scraping container logs. Co-Authored-By: Claude Opus 4.7 --- apps/journal/app/lib/sync/providers/wahoo.ts | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/journal/app/lib/sync/providers/wahoo.ts b/apps/journal/app/lib/sync/providers/wahoo.ts index f9f998f..210132a 100644 --- a/apps/journal/app/lib/sync/providers/wahoo.ts +++ b/apps/journal/app/lib/sync/providers/wahoo.ts @@ -36,16 +36,19 @@ export const wahooProvider: SyncProvider = { async exchangeCode(code: string, redirectUri: string): Promise { const resp = await fetch(`${WAHOO_AUTH}/token`, { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ client_id: clientId(), client_secret: clientSecret(), code, grant_type: "authorization_code", redirect_uri: redirectUri, - }), + }).toString(), }); - if (!resp.ok) throw new Error(`Wahoo token exchange failed: ${resp.status}`); + if (!resp.ok) { + const text = await resp.text().catch(() => ""); + throw new Error(`Wahoo token exchange failed: ${resp.status} ${text}`); + } const data = await resp.json() as { access_token: string; refresh_token: string; expires_in: number }; // Fetch user info to get provider user ID @@ -65,15 +68,18 @@ export const wahooProvider: SyncProvider = { async refreshToken(refreshToken: string): Promise { const resp = await fetch(`${WAHOO_AUTH}/token`, { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ client_id: clientId(), client_secret: clientSecret(), grant_type: "refresh_token", refresh_token: refreshToken, - }), + }).toString(), }); - if (!resp.ok) throw new Error(`Wahoo token refresh failed: ${resp.status}`); + if (!resp.ok) { + const text = await resp.text().catch(() => ""); + throw new Error(`Wahoo token refresh failed: ${resp.status} ${text}`); + } const data = await resp.json() as { access_token: string; refresh_token: string; expires_in: number }; return { accessToken: data.access_token,