Surface Etsy OAuth errors and use form-encoded token requests
- Callback failures now redirect with the underlying error message, shown in the Settings toast, instead of a generic failure - Token endpoint requests use application/x-www-form-urlencoded per RFC 6749 instead of JSON Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
64aae6adf7
commit
08160775e7
3 changed files with 19 additions and 8 deletions
|
|
@ -51,7 +51,11 @@ const Settings = () => {
|
|||
toast.success('Etsy shop connected!');
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
} else if (etsyResult === 'error') {
|
||||
toast.error('Etsy connection failed. Please try again.');
|
||||
const reason = params.get('reason');
|
||||
toast.error(
|
||||
reason ? `Etsy connection failed: ${reason}` : 'Etsy connection failed. Please try again.',
|
||||
{ duration: 12000 }
|
||||
);
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
}
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -44,14 +44,15 @@ const loadConfig = async (userId: string): Promise<EtsyConfig | null> => {
|
|||
|
||||
// Where to send the browser after the OAuth callback: same origin as the
|
||||
// redirect URI (the app and API share one domain)
|
||||
const settingsUrl = (redirectUri: string | undefined, suffix: string): string => {
|
||||
const settingsUrl = (redirectUri: string | undefined, suffix: string, reason?: string): string => {
|
||||
let origin = process.env.CLIENT_URL || 'http://localhost:3000';
|
||||
try {
|
||||
if (redirectUri) origin = new URL(redirectUri).origin;
|
||||
} catch {
|
||||
// keep fallback
|
||||
}
|
||||
return `${origin}/settings?etsy=${suffix}`;
|
||||
const reasonParam = reason ? `&reason=${encodeURIComponent(reason.slice(0, 300))}` : '';
|
||||
return `${origin}/settings?etsy=${suffix}${reasonParam}`;
|
||||
};
|
||||
|
||||
const maskKey = (key: string): string =>
|
||||
|
|
@ -137,7 +138,12 @@ router.get('/callback', async (req: Request, res: Response) => {
|
|||
const pending = typeof state === 'string' ? pendingAuth.get(state) : undefined;
|
||||
|
||||
if (error || typeof code !== 'string' || !pending) {
|
||||
return res.redirect(settingsUrl(pending?.redirectUri, 'error'));
|
||||
const reason = error
|
||||
? `Etsy returned: ${error}${req.query.error_description ? ` — ${req.query.error_description}` : ''}`
|
||||
: !pending
|
||||
? 'Login attempt expired or unknown (possibly a server restart) — try connecting again'
|
||||
: 'No authorization code returned';
|
||||
return res.redirect(settingsUrl(pending?.redirectUri, 'error', reason));
|
||||
}
|
||||
pendingAuth.delete(String(state));
|
||||
|
||||
|
|
@ -163,9 +169,9 @@ router.get('/callback', async (req: Request, res: Response) => {
|
|||
);
|
||||
|
||||
res.redirect(settingsUrl(pending.redirectUri, 'connected'));
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('Etsy OAuth callback failed:', err);
|
||||
res.redirect(settingsUrl(pending.redirectUri, 'error'));
|
||||
res.redirect(settingsUrl(pending.redirectUri, 'error', err?.message || 'Token exchange failed'));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@ interface TokenResponse {
|
|||
}
|
||||
|
||||
const requestToken = async (body: Record<string, string>): Promise<TokenResponse> => {
|
||||
// OAuth 2.0 token endpoints take form-encoded bodies (RFC 6749)
|
||||
const res = await fetch(ETSY_TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(body).toString(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Etsy token request failed (${res.status}): ${await res.text()}`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue