HEAD שיקר לי, ואני האמנתי
שתי הודעות יושבות ב-prod-telegram-alerts-dlq. שתיהן אותו משתמש, שתיהן Yad2, שתיהן TelegramNetworkError: Request timeout אחרי 4 ניסיונות. הלוגים מראים בדיוק ~21 שניות בין send_photo לכישלון. החשד הראשון שלי היה הגיוני: אולי התמונה עצמה שבורה, ו-Telegram שמושך אותה צד-שרת תקוע על ה-fetch.
הרצתי curl -I על ה-URL של התמונה כדי לאשר:
HTTP/2 200
content-type: image/jpeg
content-length: 0
x-cache: Hit from cloudfront
אפס בייטים. הינה ההוכחה. כתבתי אבחנה מסודרת — CloudFront מחזיר מטמון של תגובה ריקה, Telegram מנסה למשוך, נתקע, אנחנו נופלים ב-timeout. גם הצעתי תיקון: HEAD לפני העברה ל-Telegram, ואם content-length אפס — לעבור ל-fallback של טקסט.
המשתמש כתב לי משפט אחד: “אבל אני פותח את התמונות בכרום.”
הבדיקה שהייתי צריך לעשות מההתחלה
curl -s "$U" -A "Mozilla/5.0 ..." -o /tmp/img.jpg
# size=150388 type=image/jpeg
# JPEG image data, baseline, 1024x473
150KB. תמונה תקינה לחלוטין. אותו URL בדיוק שאמרתי שהוא ריק.
מה שקרה: CloudFront (או origin של Yad2) מחזיר content-length: 0 בתגובה ל-HEAD, אבל בתגובה ל-GET מחזיר את התמונה המלאה. זו לא הפעם הראשונה שאני נתקל ב-edge case כזה — יש שרתי origin שלא טורחים להריץ את ה-handler המלא ל-HEAD ופשוט מחזירים headers ריקים. ה-RFC אומר ש-HEAD “צריך” להחזיר את אותם headers כמו GET. “צריך” עושה הרבה עבודה במשפט הזה.
הטעות שלי לא הייתה ב-curl. הטעות שלי הייתה ש-content-length: 0 היה התשובה הראשונה שראיתי, והפסקתי לחפש. כל הרציונל שבניתי אחרי זה — Telegram תקוע על fetch ריק, CloudFront cache מורעל — היה סיפור עקבי שמתאים לעובדה אחת מוטעית. סיפור טוב מסתיר חורים יותר טוב מסיפור גרוע.
מה הבעיה האמיתית
בלי ה-HEAD המטעה, האבחנה הייתה זמינה כל הזמן בלוגים: ~21 שניות לכישלון, ובקוד _HTTP_TIMEOUT_S = 20. כשאתה קורא ל-bot.send_photo(photo=<URL>), Telegram הולך למשוך את ה-URL מהשרת שלו לפני שהוא עונה לך. אם הם ב-DC רחוק והתמונה ב-CDN ישראלי, ה-fetch הזה לוקח לפעמים 30-40 שניות. ה-timeout שלנו של 20 שניות חתך את החיבור באמצע ה-fetch הלגיטימי, aiogram זרק TelegramNetworkError, SQS ניסה שוב 4 פעמים, DLQ.
התיקון פשוט בהתאם:
# notifier.py
except (TelegramBadRequest, TelegramNetworkError) as exc:
# ... fallback ל-send_message
# telegram_sender_handler.py
_HTTP_TIMEOUT_S = 50 # היה 20
serverless.yml: Lambda timeout 60→90, SQS visibility 60→90. שרשרת הזמנים עכשיו עקבית: HTTP 50 + buffer 5 = 55 ≤ Lambda 90 ≤ SQS 90.
מה אני לוקח מזה
שני דברים. הראשון, טכני: אל תסמוך על HEAD לאימות שתמונה תקינה. אם הכוונה שלך היא “האם זה באמת בייטים תקינים”, רק GET עונה. HEAD זה אופטימיזציה שאופציונלית-נכונה.
השני, יותר חשוב: ה-content-length: 0 היה data point יחיד שתאם תיאוריה אחת. לא היו לי שני data points עצמאיים שהצביעו על אותה מסקנה. אבל היה לי data point שני שהתעלמתי ממנו — הזמן בלוגים, 21 שניות בדיוק. הוא צעק על ה-_HTTP_TIMEOUT_S = 20 כל הזמן. אני פשוט הייתי עסוק בלספר לעצמי על CloudFront.
הכל היה זמין. רק לא הסתכלתי על הדבר הנכון.