fix: add comprehensive error handling for P1 critical issues

P1-1: Callback query error handling
- Add try-catch around domain registration and server order
- Send user-friendly error messages on failure
- Use answerCallbackQuery to acknowledge button clicks
- Add structured logging with createLogger

P1-2: Queue DLQ monitoring
- Add admin notification when server provisioning fails
- Update order status to 'failed' in database
- Include detailed context in notifications
- Apply rate limiting (1 notification per hour)

P1-3: Email handler error recovery
- Add admin notification when SMS parsing fails
- Include email preview in notifications
- Mask email addresses for privacy
- Add structured logging with emailLogger

Also add 'failed' status to ServerOrder type.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-28 20:36:49 +09:00
parent 9fc6820b77
commit 97b8f3c7f7
4 changed files with 289 additions and 132 deletions

View File

@@ -9,6 +9,7 @@ import { reconcileDeposits, formatReconciliationReport } from './utils/reconcili
import { handleProvisionQueue, handleProvisionDLQ } from './server-provision';
import { timingSafeEqual } from './security';
import { createLogger } from './utils/logger';
import { notifyAdmin } from './services/notification';
const logger = createLogger('worker');
@@ -101,23 +102,50 @@ Documentation: https://github.com/your-repo
// Email 핸들러 (SMS → 메일 → 파싱)
async email(message: EmailMessage, env: Env): Promise<void> {
const emailLogger = createLogger('email-handler');
try {
// 이메일 본문 읽기
const rawEmail = await new Response(message.raw).text();
// 이메일 주소 마스킹
const maskedFrom = message.from.replace(/@.+/, '@****');
logger.info('이메일 수신', { from: maskedFrom, size: message.rawSize });
emailLogger.info('이메일 수신', { from: maskedFrom, size: message.rawSize });
// SMS 내용 파싱
const notification = await parseBankSMS(rawEmail, env);
if (!notification) {
logger.info('은행 SMS 파싱 실패');
return;
// Structured logging with context
emailLogger.warn('SMS 파싱 실패', {
from: maskedFrom,
subject: message.headers.get('subject') || 'N/A',
preview: rawEmail.substring(0, 200).replace(/\s+/g, ' '),
size: message.rawSize
});
// Admin notification for manual review
await notifyAdmin(
'api_error',
{
service: 'Email Handler',
error: 'SMS parsing failed',
context: `From: ${maskedFrom}\nSubject: ${message.headers.get('subject') || 'N/A'}\nPreview: ${rawEmail.substring(0, 150).replace(/\s+/g, ' ')}...`
},
{
telegram: {
sendMessage: (chatId: number, text: string) =>
sendMessage(env.BOT_TOKEN, chatId, text)
},
adminId: env.DEPOSIT_ADMIN_ID || '',
env
}
);
return; // Don't throw - email routing expects success
}
// 파싱 결과 마스킹 로깅
logger.info('SMS 파싱 결과', {
emailLogger.info('SMS 파싱 결과', {
bankName: notification.bankName,
depositorName: notification.depositorName
? notification.depositorName.slice(0, 2) + '***'
@@ -144,16 +172,16 @@ Documentation: https://github.com/your-repo
).run();
const notificationId = insertResult.meta.last_row_id;
logger.info('알림 저장 완료', { notificationId });
emailLogger.info('알림 저장 완료', { notificationId });
// 자동 매칭 시도
const matched = await matchPendingDeposit(env.DB, notificationId, notification);
// 매칭 결과 로깅 (민감 정보 마스킹)
if (matched) {
logger.info('자동 매칭 성공', { transactionId: matched.transactionId });
emailLogger.info('자동 매칭 성공', { transactionId: matched.transactionId });
} else {
logger.info('매칭되는 거래 없음');
emailLogger.info('매칭되는 거래 없음');
}
// 매칭 성공 시 사용자에게 알림
@@ -196,7 +224,11 @@ Documentation: https://github.com/your-repo
);
}
} catch (error) {
logger.error('이메일 처리 오류', error as Error);
emailLogger.error('이메일 처리 오류', error as Error, {
from: message.from.replace(/@.+/, '@****'),
size: message.rawSize
});
// Don't rethrow - email routing expects success response
}
},