Skip to content

Commit 2c78e46

Browse files
fix: trim API key whitespace, handle non-JSON 200s, improve timeout errors
- Trim leading/trailing whitespace from VALIDKIT_API_KEY (prevents silent 401s from copy-paste errors) - Return ok:false when response.json() fails, even on 200 status (prevents CDN challenge pages being serialized as success) - Extract formatCatchError() helper with DOMException TimeoutError detection — shows "timed out after 30 seconds" instead of "operation was aborted" - Add 3 new tests (37 total, 100% coverage)
1 parent 7782e3b commit 2c78e46

2 files changed

Lines changed: 78 additions & 13 deletions

File tree

src/index.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,5 +565,64 @@ describe('ValidKit MCP Server', () => {
565565
expect(result.isError).toBe(true);
566566
expect(getText(result)).toContain('VALIDKIT_API_KEY');
567567
});
568+
569+
it('trims whitespace from API key', async () => {
570+
process.env.VALIDKIT_API_KEY = ' vk_test_padded ';
571+
const { client } = await createTestClient();
572+
mockFetchResponse(200, { email: 'a@b.com', valid: true });
573+
574+
await client.callTool({
575+
name: 'validate_email',
576+
arguments: { email: 'a@b.com' },
577+
});
578+
579+
expect(mockFetch).toHaveBeenCalledWith(
580+
expect.any(String),
581+
expect.objectContaining({
582+
headers: expect.objectContaining({
583+
'X-API-Key': 'vk_test_padded',
584+
}),
585+
})
586+
);
587+
});
588+
589+
it('handles non-JSON response on 200 as error', async () => {
590+
const { client } = await createTestClient();
591+
mockFetch.mockResolvedValueOnce({
592+
ok: true,
593+
status: 200,
594+
json: async () => {
595+
throw new SyntaxError('Unexpected token <');
596+
},
597+
});
598+
599+
const result = await client.callTool({
600+
name: 'validate_email',
601+
arguments: { email: 'test@gmail.com' },
602+
});
603+
604+
expect(result.isError).toBe(true);
605+
expect(getText(result)).toContain('Non-JSON response');
606+
expect(getText(result)).toContain('200');
607+
});
608+
609+
it('shows user-friendly timeout error message', async () => {
610+
const { client } = await createTestClient();
611+
const timeoutError = new DOMException(
612+
'The operation was aborted',
613+
'TimeoutError'
614+
);
615+
mockFetch.mockRejectedValueOnce(timeoutError);
616+
617+
const result = await client.callTool({
618+
name: 'validate_email',
619+
arguments: { email: 'test@gmail.com' },
620+
});
621+
622+
expect(result.isError).toBe(true);
623+
expect(getText(result)).toContain('timed out');
624+
expect(getText(result)).toContain('30 seconds');
625+
expect(getText(result)).not.toContain('was aborted');
626+
});
568627
});
569628
});

src/index.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import { z } from 'zod';
88
export const VERSION = '1.1.0';
99
const REQUEST_TIMEOUT_MS = 30_000;
1010

11+
export function formatCatchError(error: unknown, prefix: string): string {
12+
if (error instanceof DOMException && error.name === 'TimeoutError') {
13+
return `${prefix}: Request timed out after ${REQUEST_TIMEOUT_MS / 1000} seconds. The ValidKit API may be slow or unreachable. Try again.`;
14+
}
15+
const message = error instanceof Error ? error.message : 'Unknown error';
16+
return `${prefix}: ${message}`;
17+
}
18+
1119
export function getApiBaseUrl(): string {
1220
const url = process.env.VALIDKIT_API_URL || 'https://api.validkit.com';
1321
if (!url.startsWith('https://')) {
@@ -25,7 +33,7 @@ export function getApiKey(): string {
2533
'VALIDKIT_API_KEY environment variable is required. Get your free API key at https://validkit.com/get-started'
2634
);
2735
}
28-
return key;
36+
return key.trim();
2937
}
3038

3139
export async function callApi(
@@ -50,9 +58,13 @@ export async function callApi(
5058
try {
5159
data = await response.json();
5260
} catch {
53-
data = {
54-
error: {
55-
message: `Non-JSON response (HTTP ${response.status})`,
61+
return {
62+
ok: false,
63+
status: response.status,
64+
data: {
65+
error: {
66+
message: `Non-JSON response (HTTP ${response.status})`,
67+
},
5668
},
5769
};
5870
}
@@ -128,13 +140,11 @@ export function createServer(): McpServer {
128140
],
129141
};
130142
} catch (error) {
131-
const message =
132-
error instanceof Error ? error.message : 'Unknown error';
133143
return {
134144
content: [
135145
{
136146
type: 'text' as const,
137-
text: `Failed to validate email: ${message}`,
147+
text: formatCatchError(error, 'Failed to validate email'),
138148
},
139149
],
140150
isError: true,
@@ -179,13 +189,11 @@ export function createServer(): McpServer {
179189
],
180190
};
181191
} catch (error) {
182-
const message =
183-
error instanceof Error ? error.message : 'Unknown error';
184192
return {
185193
content: [
186194
{
187195
type: 'text' as const,
188-
text: `Failed to validate emails: ${message}`,
196+
text: formatCatchError(error, 'Failed to validate emails'),
189197
},
190198
],
191199
isError: true,
@@ -219,13 +227,11 @@ export function createServer(): McpServer {
219227
],
220228
};
221229
} catch (error) {
222-
const message =
223-
error instanceof Error ? error.message : 'Unknown error';
224230
return {
225231
content: [
226232
{
227233
type: 'text' as const,
228-
text: `Failed to check usage: ${message}`,
234+
text: formatCatchError(error, 'Failed to check usage'),
229235
},
230236
],
231237
isError: true,

0 commit comments

Comments
 (0)