Skip to content
Open
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
21 changes: 21 additions & 0 deletions src/outbound/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ describe('classifyError', () => {
expect(err.code).toBe('target_revoked');
});

test('infers target_revoked from a withdrawn-message response without a Feishu code', () => {
const err = classifyError({
response: {
status: 400,
data: { message: 'The message was withdrawn.' },
},
});
expect(err.code).toBe('target_revoked');
expect(err.message).toBe('The message was withdrawn.');
});

test('does not classify an unrelated HTTP 400 as target_revoked', () => {
const err = classifyError({
response: {
status: 400,
data: { message: 'Invalid message format.' },
},
});
expect(err.code).toBe('format_error');
});

test('detects ssrf_blocked from error message prefix', () => {
const err = classifyError(new Error('ssrf_blocked: 10.0.0.1'));
expect(err.code).toBe('ssrf_blocked');
Expand Down
7 changes: 6 additions & 1 deletion src/outbound/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ function inferCode(err: unknown): LarkChannelErrorCode {
const raw = err as any;
const status = raw?.response?.status ?? raw?.status;
const feishuCode = raw?.response?.data?.code ?? raw?.data?.code ?? raw?.code;
const msg = String(raw?.message ?? '').toLowerCase();
const msg = extractMessage(err).toLowerCase();

if (typeof feishuCode === 'number') {
if (feishuCode === 230020 || feishuCode === 230017) return 'target_revoked';
if (feishuCode === 99991400 || feishuCode === 99991401) return 'permission_denied';
if (feishuCode === 230002 || feishuCode === 230001) return 'format_error';
}

// Feishu can return HTTP 400 without a numeric platform code when the
// message targeted by a reply has already been withdrawn. Classify the
// platform message before the generic HTTP 400 fallback.
if (/\bmessage\b.*\b(withdrawn|recalled)\b/.test(msg)) return 'target_revoked';

if (status === 429) return 'rate_limited';
if (status === 401 || status === 403) return 'permission_denied';
if (status === 400) return 'format_error';
Expand Down