diff --git a/src/lib/api.ts b/src/lib/api.ts index d42ad50..c38b020 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -41,11 +41,28 @@ async function rawRequest( if (opts.body !== undefined) headers['Content-Type'] = 'application/json'; if (token) headers.Authorization = `Bearer ${token}`; - const res = await fetch(url, { - method: opts.method ?? 'GET', - headers, - body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, - }); + let res: Response; + try { + res = await fetch(url, { + method: opts.method ?? 'GET', + headers, + body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, + }); + } catch (err) { + // Node's undici throws a bare `TypeError: fetch failed` and stashes the + // real reason (DNS, ECONNREFUSED, cert errors, …) on `cause`. Surface + // both so users can tell a misconfigured API URL from a real outage. + const cause = (err as { cause?: unknown }).cause; + const reason = + cause && typeof cause === 'object' + ? ((cause as { code?: string }).code ?? (cause as Error).message ?? '') + : ''; + throw new Error( + `Network error reaching ${url.origin}${reason ? ` (${reason})` : ''}. ` + + `Check TIMEBOOK_API_URL or your connection.`, + { cause: err }, + ); + } const text = await res.text(); let data: unknown = undefined; diff --git a/src/lib/config.ts b/src/lib/config.ts index 8805aac..0844f0c 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -21,8 +21,11 @@ export interface StoredConfig { createdAt?: string; } +// Production serves both the React app and the JSON API from the same +// origin (`usetimebook.com/api/*`). There is no `api.usetimebook.com` +// subdomain, so the API URL defaults to the same host as the web URL. const DEFAULT_CONFIG: StoredConfig = { - apiUrl: process.env.TIMEBOOK_API_URL ?? 'https://api.usetimebook.com', + apiUrl: process.env.TIMEBOOK_API_URL ?? 'https://usetimebook.com', webUrl: process.env.TIMEBOOK_WEB_URL ?? 'https://usetimebook.com', };