|
| 1 | +import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'; |
| 2 | + |
1 | 3 | import log from '@apify/log'; |
2 | 4 |
|
3 | 5 | /** |
@@ -29,28 +31,71 @@ export function getHttpStatusCode(error: unknown): number | undefined { |
29 | 31 | } |
30 | 32 |
|
31 | 33 | /** |
32 | | - * Logs HTTP errors based on status code, following apify-core pattern. |
33 | | - * Uses `softFail` for status < 500 (API client errors) and `exception` for status >= 500 (API server errors). |
| 34 | + * Client/caller faults and transient transport conditions that shouldn't trigger error alerts. |
| 35 | + * Anything else in the JSON-RPC reserved range (-32768..-32000) is treated as a server fault. |
| 36 | + */ |
| 37 | +const SOFT_MCP_ERROR_CODES: ReadonlySet<number> = new Set([ |
| 38 | + ErrorCode.ParseError, |
| 39 | + ErrorCode.InvalidRequest, |
| 40 | + ErrorCode.MethodNotFound, |
| 41 | + ErrorCode.InvalidParams, |
| 42 | + ErrorCode.ConnectionClosed, |
| 43 | + ErrorCode.RequestTimeout, |
| 44 | +]); |
| 45 | + |
| 46 | +/** |
| 47 | + * Extract a JSON-RPC error code from an `McpError`-shaped object. |
| 48 | + * Returns `undefined` if the `code` field is absent or outside the JSON-RPC reserved range. |
| 49 | + */ |
| 50 | +function getMcpErrorCode(error: unknown): number | undefined { |
| 51 | + if (typeof error !== 'object' || error === null || !('code' in error)) return undefined; |
| 52 | + const { code } = error as { code?: unknown }; |
| 53 | + if (typeof code === 'number' && code >= -32768 && code <= -32000) return code; |
| 54 | + return undefined; |
| 55 | +} |
| 56 | + |
| 57 | +/** |
| 58 | + * Logs HTTP or MCP errors at the appropriate level: |
| 59 | + * - Client errors (HTTP < 500, or JSON-RPC client/transient codes) → softFail (no stack). |
| 60 | + * - Server errors (HTTP >= 500, or JSON-RPC server codes) → exception (with stack). |
| 61 | + * - Anything unclassifiable → error. |
34 | 62 | * |
35 | 63 | * @param error - The error object |
36 | 64 | * @param message - The log message |
37 | 65 | * @param data - Additional data to include in the log |
38 | 66 | */ |
39 | 67 | export function logHttpError<T extends object>(error: unknown, message: string, data?: T): void { |
40 | 68 | const statusCode = getHttpStatusCode(error); |
41 | | - const errorMessage = error instanceof Error ? error.message : String(error); |
| 69 | + const rawErrorMessage = error instanceof Error ? error.message : String(error); |
| 70 | + // Mezmo (logDNA) promotes log entries to error level when message/keys contain "error". |
| 71 | + // Sanitize for softFail paths (see CONTRIBUTING.md § Logging → Mezmo promotion rule). |
| 72 | + const softErrMessage = rawErrorMessage.replace(/ error:/gi, ' failure:'); |
42 | 73 |
|
43 | 74 | if (statusCode !== undefined && statusCode < 500) { |
44 | | - // Client errors (< 500) - log as softFail without stack trace |
45 | | - log.softFail(message, { errMessage: errorMessage, statusCode, ...data }); |
46 | | - } else if (statusCode !== undefined && statusCode >= 500) { |
47 | | - // Server errors (>= 500) - log as exception with full error (includes stack trace) |
| 75 | + // HTTP client errors (< 500) - softFail without stack trace |
| 76 | + log.softFail(message, { errMessage: softErrMessage, statusCode, ...data }); |
| 77 | + return; |
| 78 | + } |
| 79 | + if (statusCode !== undefined && statusCode >= 500) { |
| 80 | + // HTTP server errors (>= 500) - exception with full error (includes stack trace) |
48 | 81 | const errorObj = error instanceof Error ? error : new Error(String(error)); |
49 | 82 | log.exception(errorObj, message, { statusCode, ...data }); |
50 | | - } else { |
51 | | - // No status code available - log as error |
52 | | - log.error(message, { error, ...data }); |
| 83 | + return; |
53 | 84 | } |
| 85 | + |
| 86 | + const mcpErrorCode = getMcpErrorCode(error); |
| 87 | + if (mcpErrorCode !== undefined) { |
| 88 | + if (SOFT_MCP_ERROR_CODES.has(mcpErrorCode)) { |
| 89 | + log.softFail(message, { errMessage: softErrMessage, mcpErrorCode, ...data }); |
| 90 | + } else { |
| 91 | + const errorObj = error instanceof Error ? error : new Error(String(error)); |
| 92 | + log.exception(errorObj, message, { mcpErrorCode, ...data }); |
| 93 | + } |
| 94 | + return; |
| 95 | + } |
| 96 | + |
| 97 | + // No status code available - log as error |
| 98 | + log.error(message, { error, ...data }); |
54 | 99 | } |
55 | 100 |
|
56 | 101 | const SKYFIRE_PAY_ID_KEY = 'skyfire-pay-id'; |
|
0 commit comments