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
8 changes: 8 additions & 0 deletions .changeset/gentle-gifts-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"lingo.dev": patch
"@lingo.dev/_sdk": patch
---

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.

CLI: Auth failures now show specific error messages (e.g., "Invalid API key" vs generic "Authentication failed").
12 changes: 8 additions & 4 deletions packages/cli/src/cli/localizer/lingodotdev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@ export default function createLingoDotDevLocalizer(
checkAuth: async () => {
try {
const response = await engine.whoami();
return {
authenticated: !!response,
username: response?.email,
};
if (!response) {
return {
authenticated: false,
error:
"Invalid API key. Run `lingo.dev login` or check your LINGO_API_KEY.",
};
}
return { authenticated: true, username: response.email };
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
Expand Down
111 changes: 59 additions & 52 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,39 @@ export class LingoDotDevEngine {
};
}

private static async extractErrorMessage(res: Response): Promise<string> {
try {
const text = await res.text();
const parsed = JSON.parse(text);
if (parsed && typeof parsed.message === "string") {
return parsed.message;
}
if (parsed?._tag === "NotFoundError") {
return `${parsed.entityType} not found: ${parsed.id}`;
Comment on lines +53 to +54
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add defensive checks for NotFoundError properties.

If the server sends { "_tag": "NotFoundError" } without entityType or id, this will produce the message "undefined not found: undefined". Consider adding fallback values.

Proposed fix
       if (parsed?._tag === "NotFoundError") {
-        return `${parsed.entityType} not found: ${parsed.id}`;
+        return `${parsed.entityType ?? "Entity"} not found: ${parsed.id ?? "unknown"}`;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (parsed?._tag === "NotFoundError") {
return `${parsed.entityType} not found: ${parsed.id}`;
if (parsed?._tag === "NotFoundError") {
return `${parsed.entityType ?? "Entity"} not found: ${parsed.id ?? "unknown"}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk/src/index.ts` around lines 53 - 54, The NotFoundError branch
uses parsed.entityType and parsed.id directly which can be undefined; update the
NotFoundError handling (the branch checking parsed?._tag === "NotFoundError") to
use safe fallbacks (e.g., default entityType to "entity" or "unknown entity" and
id to "unknown id" or empty string) when constructing the message so it never
produces "undefined not found: undefined".

}
return text;
} catch {
return `Unexpected error (${res.status})`;
}
}

private static async throwOnHttpError(
res: Response,
context?: string,
): Promise<void> {
if (res.ok) return;
const msg = await LingoDotDevEngine.extractErrorMessage(res);
if (res.status >= 500 && res.status < 600) {
throw new Error(
`Server error (${res.status}): ${msg}. This may be due to temporary service issues.`,
);
}
if (res.status === 400) {
throw new Error(`Invalid request: ${msg}`);
}
throw new Error(context ? `${context}: ${msg}` : msg);
}

/**
* Create a new LingoDotDevEngine instance
* @param config - Configuration options for the Engine
Expand Down Expand Up @@ -76,7 +109,6 @@ export class LingoDotDevEngine {
const chunkedPayload = this.extractPayloadChunks(finalPayload);
const processedPayloadChunks: Record<string, string>[] = [];

const workflowId = createId();
for (let i = 0; i < chunkedPayload.length; i++) {
const chunk = chunkedPayload[i];
const percentageCompleted = Math.round(
Expand All @@ -87,7 +119,6 @@ export class LingoDotDevEngine {
finalParams.sourceLocale,
finalParams.targetLocale,
{ data: chunk, reference: params.reference, hints: params.hints },
workflowId,
params.fast || false,
params.filePath,
params.triggerType,
Expand All @@ -109,7 +140,6 @@ export class LingoDotDevEngine {
* @param sourceLocale - Source locale
* @param targetLocale - Target locale
* @param payload - Payload containing the chunk to be localized
* @param workflowId - Workflow ID for tracking
* @param fast - Whether to use fast mode
* @param filePath - Optional file path for metadata
* @param triggerType - Optional trigger type
Expand All @@ -124,7 +154,6 @@ export class LingoDotDevEngine {
reference?: Z.infer<typeof referenceSchema>;
hints?: Z.infer<typeof hintsSchema>;
},
workflowId: string,
fast: boolean,
filePath?: string,
triggerType?: "cli" | "ci",
Expand Down Expand Up @@ -152,19 +181,7 @@ export class LingoDotDevEngine {
signal,
});

if (!res.ok) {
if (res.status >= 500 && res.status < 600) {
const errorText = await res.text();
throw new Error(
`Server error (${res.status}): ${res.statusText}. ${errorText}. This may be due to temporary service issues.`,
);
} else if (res.status === 400) {
throw new Error(`Invalid request: ${res.statusText}`);
} else {
const errorText = await res.text();
throw new Error(errorText);
}
}
await LingoDotDevEngine.throwOnHttpError(res);

const jsonResponse = await res.json();

Expand Down Expand Up @@ -723,14 +740,10 @@ export class LingoDotDevEngine {
signal,
});

if (!response.ok) {
if (response.status >= 500 && response.status < 600) {
throw new Error(
`Server error (${response.status}): ${response.statusText}. This may be due to temporary service issues.`,
);
}
throw new Error(`Error recognizing locale: ${response.statusText}`);
}
await LingoDotDevEngine.throwOnHttpError(
response,
"Error recognizing locale",
);

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

try {
const res = await fetch(url, {
method: "GET",
headers: this.headers,
signal,
});

if (res.ok) {
const payload = await res.json();
if (!payload?.email) {
return null;
}
const res = await fetch(url, {
method: "GET",
headers: this.headers,
signal,
});

return {
email: payload.email,
id: payload.id,
};
if (res.ok) {
const payload = await res.json();
if (!payload?.email) {
return null;
}

if (res.status >= 500 && res.status < 600) {
throw new Error(
`Server error (${res.status}): ${res.statusText}. This may be due to temporary service issues.`,
);
}
return {
email: payload.email,
id: payload.id,
};
}

return null;
} catch (error) {
if (error instanceof Error && error.message.includes("Server error")) {
throw error;
}
return null;
if (res.status >= 500 && res.status < 600) {
const msg = await LingoDotDevEngine.extractErrorMessage(res);
throw new Error(
`Server error (${res.status}): ${msg}. This may be due to temporary service issues.`,
);
}

return null;
}
}

Expand Down
Loading