Skip to content

Commit 9991515

Browse files
authored
fix: improve API error messages and auth failure UX (#2051)
* fix: improve API error messages and auth failure UX
1 parent 48f8b3b commit 9991515

File tree

3 files changed

+75
-56
lines changed

3 files changed

+75
-56
lines changed

.changeset/gentle-gifts-tell.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"lingo.dev": patch
3+
"@lingo.dev/_sdk": patch
4+
---
5+
6+
SDK: Improved API error messages by parsing server JSON responses instead of using HTTP status text. Removed try/catch from whoami so network errors propagate instead of being silently treated as "not authenticated". Deduplicated error handling into shared helpers. Removed unused workflowId parameter.
7+
8+
CLI: Auth failures now show specific error messages (e.g., "Invalid API key" vs generic "Authentication failed").

packages/cli/src/cli/localizer/lingodotdev.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ export default function createLingoDotDevLocalizer(
3939
checkAuth: async () => {
4040
try {
4141
const response = await engine.whoami();
42-
return {
43-
authenticated: !!response,
44-
username: response?.email,
45-
};
42+
if (!response) {
43+
return {
44+
authenticated: false,
45+
error:
46+
"Invalid API key. Run `lingo.dev login` or check your LINGO_API_KEY.",
47+
};
48+
}
49+
return { authenticated: true, username: response.email };
4650
} catch (error) {
4751
const errorMessage =
4852
error instanceof Error ? error.message : String(error);

packages/sdk/src/index.ts

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,39 @@ export class LingoDotDevEngine {
4343
};
4444
}
4545

46+
private static async extractErrorMessage(res: Response): Promise<string> {
47+
try {
48+
const text = await res.text();
49+
const parsed = JSON.parse(text);
50+
if (parsed && typeof parsed.message === "string") {
51+
return parsed.message;
52+
}
53+
if (parsed?._tag === "NotFoundError") {
54+
return `${parsed.entityType} not found: ${parsed.id}`;
55+
}
56+
return text;
57+
} catch {
58+
return `Unexpected error (${res.status})`;
59+
}
60+
}
61+
62+
private static async throwOnHttpError(
63+
res: Response,
64+
context?: string,
65+
): Promise<void> {
66+
if (res.ok) return;
67+
const msg = await LingoDotDevEngine.extractErrorMessage(res);
68+
if (res.status >= 500 && res.status < 600) {
69+
throw new Error(
70+
`Server error (${res.status}): ${msg}. This may be due to temporary service issues.`,
71+
);
72+
}
73+
if (res.status === 400) {
74+
throw new Error(`Invalid request: ${msg}`);
75+
}
76+
throw new Error(context ? `${context}: ${msg}` : msg);
77+
}
78+
4679
/**
4780
* Create a new LingoDotDevEngine instance
4881
* @param config - Configuration options for the Engine
@@ -76,7 +109,6 @@ export class LingoDotDevEngine {
76109
const chunkedPayload = this.extractPayloadChunks(finalPayload);
77110
const processedPayloadChunks: Record<string, string>[] = [];
78111

79-
const workflowId = createId();
80112
for (let i = 0; i < chunkedPayload.length; i++) {
81113
const chunk = chunkedPayload[i];
82114
const percentageCompleted = Math.round(
@@ -87,7 +119,6 @@ export class LingoDotDevEngine {
87119
finalParams.sourceLocale,
88120
finalParams.targetLocale,
89121
{ data: chunk, reference: params.reference, hints: params.hints },
90-
workflowId,
91122
params.fast || false,
92123
params.filePath,
93124
params.triggerType,
@@ -109,7 +140,6 @@ export class LingoDotDevEngine {
109140
* @param sourceLocale - Source locale
110141
* @param targetLocale - Target locale
111142
* @param payload - Payload containing the chunk to be localized
112-
* @param workflowId - Workflow ID for tracking
113143
* @param fast - Whether to use fast mode
114144
* @param filePath - Optional file path for metadata
115145
* @param triggerType - Optional trigger type
@@ -124,7 +154,6 @@ export class LingoDotDevEngine {
124154
reference?: Z.infer<typeof referenceSchema>;
125155
hints?: Z.infer<typeof hintsSchema>;
126156
},
127-
workflowId: string,
128157
fast: boolean,
129158
filePath?: string,
130159
triggerType?: "cli" | "ci",
@@ -152,19 +181,7 @@ export class LingoDotDevEngine {
152181
signal,
153182
});
154183

155-
if (!res.ok) {
156-
if (res.status >= 500 && res.status < 600) {
157-
const errorText = await res.text();
158-
throw new Error(
159-
`Server error (${res.status}): ${res.statusText}. ${errorText}. This may be due to temporary service issues.`,
160-
);
161-
} else if (res.status === 400) {
162-
throw new Error(`Invalid request: ${res.statusText}`);
163-
} else {
164-
const errorText = await res.text();
165-
throw new Error(errorText);
166-
}
167-
}
184+
await LingoDotDevEngine.throwOnHttpError(res);
168185

169186
const jsonResponse = await res.json();
170187

@@ -723,14 +740,10 @@ export class LingoDotDevEngine {
723740
signal,
724741
});
725742

726-
if (!response.ok) {
727-
if (response.status >= 500 && response.status < 600) {
728-
throw new Error(
729-
`Server error (${response.status}): ${response.statusText}. This may be due to temporary service issues.`,
730-
);
731-
}
732-
throw new Error(`Error recognizing locale: ${response.statusText}`);
733-
}
743+
await LingoDotDevEngine.throwOnHttpError(
744+
response,
745+
"Error recognizing locale",
746+
);
734747

735748
const jsonResponse = await response.json();
736749
trackEvent(
@@ -759,38 +772,32 @@ export class LingoDotDevEngine {
759772
): Promise<{ email: string; id: string } | null> {
760773
const url = `${this.config.apiUrl}/users/me`;
761774

762-
try {
763-
const res = await fetch(url, {
764-
method: "GET",
765-
headers: this.headers,
766-
signal,
767-
});
768-
769-
if (res.ok) {
770-
const payload = await res.json();
771-
if (!payload?.email) {
772-
return null;
773-
}
775+
const res = await fetch(url, {
776+
method: "GET",
777+
headers: this.headers,
778+
signal,
779+
});
774780

775-
return {
776-
email: payload.email,
777-
id: payload.id,
778-
};
781+
if (res.ok) {
782+
const payload = await res.json();
783+
if (!payload?.email) {
784+
return null;
779785
}
780786

781-
if (res.status >= 500 && res.status < 600) {
782-
throw new Error(
783-
`Server error (${res.status}): ${res.statusText}. This may be due to temporary service issues.`,
784-
);
785-
}
787+
return {
788+
email: payload.email,
789+
id: payload.id,
790+
};
791+
}
786792

787-
return null;
788-
} catch (error) {
789-
if (error instanceof Error && error.message.includes("Server error")) {
790-
throw error;
791-
}
792-
return null;
793+
if (res.status >= 500 && res.status < 600) {
794+
const msg = await LingoDotDevEngine.extractErrorMessage(res);
795+
throw new Error(
796+
`Server error (${res.status}): ${msg}. This may be due to temporary service issues.`,
797+
);
793798
}
799+
800+
return null;
794801
}
795802
}
796803

0 commit comments

Comments
 (0)