Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/desktop/src/main/billing/entitlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
* authenticated /api/auth/session endpoint and cache it briefly so the
* streaming gate doesn't hit the network on every start. On any failure we
* fail closed to 'free' (YouTube-only), never open.
*
* Failing closed only stays fair if a blip isn't mistaken for a failure, so the
* session lookup goes through apiFetch: a transient connect timeout is retried
* rather than silently downgrading a paying user for the length of the cache.
*/

import type { Plan, Profile } from '@pairux/shared-types';
import { effectivePlan } from '@pairux/shared-types';
import { API_BASE_URL } from '../../shared/config';
import { getValidAuth } from '../auth/secure-storage';
import { apiFetch } from '../lib/apiFetch';

const CACHE_TTL_MS = 60_000;

Expand All @@ -31,7 +36,7 @@ async function fetchPlanFromServer(): Promise<Plan> {
if (!stored) return 'free';

try {
const response = await fetch(`${API_BASE_URL}/api/auth/session`, {
const response = await apiFetch(`${API_BASE_URL}/api/auth/session`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${stored.accessToken}`,
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src/main/ipc/network-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,20 @@ describe('formatNetworkError', () => {
expect(formatNetworkError(error)).toContain('DNS lookup failed');
expect(formatNetworkError(error)).toContain('ENOTFOUND');
});

// undici's connect timeout carries no hostname/syscall, only a code and a
// message, and it is the code most likely to reach a user: apiFetch retries
// it, so seeing it at all means the retries were already spent.
it('gives undici connect timeouts friendly text', () => {
const error = new Error('fetch failed') as Error & {
cause?: Record<string, unknown>;
};
error.cause = {
code: 'UND_ERR_CONNECT_TIMEOUT',
message: 'Connect Timeout Error (attempted address: pairux.com:443, timeout: 10000ms)',
};

expect(formatNetworkError(error)).toContain('Connection timed out');
expect(formatNetworkError(error)).toContain('UND_ERR_CONNECT_TIMEOUT');
});
});
9 changes: 9 additions & 0 deletions apps/desktop/src/main/ipc/network-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ interface FetchCause {
message?: string;
}

// Codes apiFetch retries only reach a user once those retries are spent, so
// each one needs text worth reading. UND_ERR_HEADERS_TIMEOUT is not retried —
// the request was already sent, so a POST can't be replayed safely — but it
// surfaces the same way, so it is spelled out here too.
const FRIENDLY_CODES: Record<string, string> = {
ENOTFOUND: 'DNS lookup failed',
EAI_AGAIN: 'DNS lookup timed out',
ECONNREFUSED: 'Connection refused',
ECONNRESET: 'Connection reset',
ETIMEDOUT: 'Connection timed out',
ENETUNREACH: 'Network unreachable',
EHOSTUNREACH: 'Host unreachable',
UND_ERR_CONNECT_TIMEOUT: 'Connection timed out',
UND_ERR_HEADERS_TIMEOUT: 'Server took too long to respond',
UND_ERR_SOCKET: 'Connection closed unexpectedly',
CERT_HAS_EXPIRED: 'TLS certificate expired',
DEPTH_ZERO_SELF_SIGNED_CERT: 'TLS certificate is self-signed',
UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'TLS certificate verification failed',
Expand Down
Loading