[{"data":1,"prerenderedAt":1437},["ShallowReactive",2],{"posts":3,"\u002Ffullstack\u002Ffetch-http-errors-and-timeouts":205},[4,21,32,42,52,61,69,77,86,94,102,110,119,127,135,148,162,173,183,194],{"path":5,"title":6,"description":7,"type":8,"level":9,"minutes":10,"date":11,"cover":12,"coverAlt":13,"featured":14,"tags":15,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-cloudflare-tunnel","גישה ל-Home Assistant מחוץ לבית עם Cloudflare Tunnel","מחברים את Home Assistant לכתובת משלכם בלי לפתוח פורט בנתב. מדריך ל-Home Assistant OS ול-Docker, עם הרשאות גישה ובדיקות מהטלפון.","מדריך","רמת ביניים",14,"2026-10-04","\u002Fimages\u002Fposts\u002Fhome-assistant-cloudflare-tunnel.svg","טלפון מחוץ לבית מתחבר דרך Cloudflare Tunnel ומחבר מקומי ל-Home Assistant בבית.",false,[16,17,18,19],"Home Assistant","Cloudflare","Docker","בית חכם","smart-home",{"path":22,"title":23,"description":24,"type":8,"level":25,"minutes":26,"date":27,"cover":28,"coverAlt":29,"featured":14,"tags":30,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-backup-update-recovery","גיבוי שאפשר לשחזר, ועדכון שאפשר להתאושש ממנו","מגדירים עותק גיבוי מחוץ לשרת, שומרים מפתח הצפנה ובודקים שחזור מבודד לפני שמעדכנים את הבית הפעיל.","למתחילים",8,"2026-09-29","\u002Fimages\u002Fposts\u002Fhome-assistant-backup-update-recovery.svg","שרת מקומי, ארכיון גיבוי ועותק חיצוני מחוברים למסלול שחזור.",[16,18,31],"גיבוי",{"path":33,"title":34,"description":35,"type":8,"level":25,"minutes":26,"date":36,"cover":37,"coverAlt":38,"featured":14,"tags":39,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-device-maintenance","חיישן נעלם? תחזוקה של מכשירים וסוללות","בונים רשימת תחזוקה קצרה, מפרידים בין סוללה חלשה למכשיר לא זמין ומאתרים בעיות דיווח בלי לאפס את הרשת.","2026-09-24","\u002Fimages\u002Fposts\u002Fhome-assistant-device-maintenance.svg","סוללה חלשה, רשת מכשירים עם צומת לא זמין וכלי תחזוקה.",[16,40,41],"חיישנים","Zigbee",{"path":43,"title":44,"description":45,"type":8,"level":25,"minutes":26,"date":46,"cover":47,"coverAlt":48,"featured":14,"tags":49,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-washing-machine-finished","מכונת הכביסה סיימה: התראה לפי צריכת חשמל","מודדים מחזור כביסה, מגדירים ספי הספק מתוך הנתונים ושולחים התראה רק אחרי ריצה שזוהתה, עם טיפול בדיווח חסר.","2026-09-19","\u002Fimages\u002Fposts\u002Fhome-assistant-washing-machine-finished.svg","מכונת כביסה מחוברת לגרף צריכת הספק ולהודעת סיום בטלפון.",[16,50,51,40],"אוטומציות","YAML",{"path":53,"title":54,"description":55,"type":8,"level":25,"minutes":56,"date":57,"cover":58,"coverAlt":59,"featured":14,"tags":60,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-smart-button-actions","כפתור חכם במקום עוד אפליקציה","מחברים כפתור פיזי לפעולה יומיומית, מזהים איך האינטגרציה מדווחת על לחיצות ובודקים תגובה בלי להסתמך על דגם מסוים.",7,"2026-09-14","\u002Fimages\u002Fposts\u002Fhome-assistant-smart-button-actions.svg","יד לוחצת על כפתור פיזי שמפעיל תאורה ותריס במסגרת סצנה.",[16,50,41],{"path":62,"title":63,"description":64,"type":8,"level":25,"minutes":56,"date":65,"cover":66,"coverAlt":67,"featured":14,"tags":68,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-helpers-house-modes","Helpers: נותנים לבית מצבים וזיכרון","מוסיפים מצב אורחים שנשלט מלוח הבקרה, מחברים אותו לתנאי באוטומציה ובודקים מה קורה אחרי אתחול.","2026-09-09","\u002Fimages\u002Fposts\u002Fhome-assistant-helpers-house-modes.svg","בורר מצבי בית מחובר למתגי עזר שמייצגים אורחים, יציאה והשהיה.",[16,50,51],{"path":70,"title":71,"description":72,"type":8,"level":25,"minutes":56,"date":73,"cover":74,"coverAlt":75,"featured":14,"tags":76,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-automation-debugging","האוטומציה לא עבדה: מוצאים את הסיבה","בודקים טריגרים, תנאים ופעולות לפי הסדר, קוראים Trace ומבחינים בין תקלה בחיישן לבין כלל שלא התקיים.","2026-09-04","\u002Fimages\u002Fposts\u002Fhome-assistant-automation-debugging.svg","מסלול אוטומציה מתפצל לתנאי ולפעולה, וזכוכית מגדלת מדגישה את נקודת הבדיקה.",[16,50,51],{"path":78,"title":79,"description":80,"type":8,"level":25,"minutes":56,"date":81,"cover":82,"coverAlt":83,"featured":14,"tags":84,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-light-manual-control","האור נכבה כשאתם עדיין בחדר? שיפור אוטומציית התאורה","משפרים תאורה לפי תנועה בעזרת כיבוי נפרד ומתג להשארת אור, ובודקים מה קורה בזמן קריאה, בהפעלה מחדש ובשחרור השליטה הידנית.","2026-08-30","\u002Fimages\u002Fposts\u002Fhome-assistant-light-manual-control.svg","חיישן תנועה, אדם בחדר ומנורה מופיעים לצד מתג שמאפשר להשהות כיבוי אוטומטי.",[16,19,50,85],"תאורה",{"path":87,"title":88,"description":89,"type":8,"level":25,"minutes":26,"date":90,"cover":91,"coverAlt":92,"featured":14,"tags":93,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-presence-detection","מי בבית? זיהוי נוכחות בלי כיבויים מפתיעים","מחברים את הטלפון לישות Person, בודקים זיהוי כניסה ויציאה ובונים התראת עזיבה שממתינה ולא מתייחסת למידע חסר כאל בית ריק.","2026-08-25","\u002Fimages\u002Fposts\u002Fhome-assistant-presence-detection.svg","שני אנשים מחוץ לבית, אזור נוכחות סביב הבית ושעון להשהיית פעולות יציאה.",[16,19,50],{"path":95,"title":96,"description":97,"type":8,"level":25,"minutes":56,"date":98,"cover":99,"coverAlt":100,"featured":14,"tags":101,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-useful-notifications","התראות שימושיות בלי להציף את הטלפון","מגדירים התראה כשהדלת נשארת פתוחה, בודקים הודעת ניסיון ומונעים הודעות חוזרות על כל פתיחה קצרה.","2026-08-20","\u002Fimages\u002Fposts\u002Fhome-assistant-useful-notifications.svg","דלת פתוחה מחוברת לשעון המתנה ולהתראה אחת בטלפון.",[16,19,50],{"path":103,"title":104,"description":105,"type":8,"level":25,"minutes":56,"date":106,"cover":107,"coverAlt":108,"featured":14,"tags":109,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-scenes-scripts","לילה טוב בלחיצה אחת: סצנות וסקריפטים","יוצרים סצנת ערב וסקריפט ״לילה טוב״ ב-Home Assistant, ומבינים מתי לשמור מצבים ומתי להריץ רצף פעולות.","2026-08-15","\u002Fimages\u002Fposts\u002Fhome-assistant-scenes-scripts.svg","כפתור אחד מפעיל רצף פעולות שמוביל לבית במצב לילה.",[16,19,50],{"path":111,"title":112,"description":113,"type":8,"level":25,"minutes":56,"date":114,"cover":115,"coverAlt":116,"featured":14,"tags":117,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-family-dashboard","לוח בקרה שבני הבית באמת ישתמשו בו","בונים לוח בקרה מובנה ב-Home Assistant עם חדרים, כפתורים ברורים ותצוגה נוחה בטלפון, בלי להתקין הרחבות.","2026-08-10","\u002Fimages\u002Fposts\u002Fhome-assistant-family-dashboard.svg","לוח בקרה גדול עם כפתורי תאורה וטמפרטורה מופיע לצד גרסה מותאמת לטלפון.",[16,19,118],"Dashboard",{"path":120,"title":121,"description":122,"type":8,"level":25,"minutes":56,"date":123,"cover":124,"coverAlt":125,"featured":14,"tags":126,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-organize-devices","עושים סדר בבית: חדרים, מכשירים וישויות","מסדרים את Home Assistant לפי חדרים, מבינים מה ההבדל בין מכשיר לישות ובוחרים שמות שקל למצוא בלי לשבור אוטומציות.","2026-08-05","\u002Fimages\u002Fposts\u002Fhome-assistant-organize-devices.svg","תוכנית חדרים מחוברת למכשיר אחד ולשלוש ישויות נפרדות.",[16,19,50],{"path":128,"title":129,"description":130,"type":8,"level":25,"minutes":26,"date":131,"cover":132,"coverAlt":133,"featured":14,"tags":134,"topic":20},"\u002Fsmart-home\u002Fhome-assistant-hallway-motion-light","תנועה במסדרון, אור שנדלק: אוטומציה ראשונה ב־Home Assistant","מדליקים תאורה כשמזוהה תנועה ומכבים אחרי שתי דקות ללא תנועה, עם YAML מלא והסבר על טריגרים ובדיקות.","2026-07-31","\u002Fimages\u002Fposts\u002Fhome-assistant-hallway-motion-light.svg","מסדרון ביתי עם חיישן תנועה ותאורת תקרה שנדלקת כשאדם נכנס",[16,50,85,51],{"path":136,"title":137,"description":138,"type":8,"level":9,"minutes":26,"date":139,"cover":140,"coverAlt":141,"featured":14,"tags":142,"topic":147},"\u002Ffullstack\u002Ffetch-http-errors-and-timeouts","למה Fetch לא זורק שגיאה כשהשרת מחזיר 404?","ההבדל בין כישלון רשת לתשובת HTTP, טיפול בגוף תשובה לא צפוי והגבלת זמן לבקשה, עם פונקציה שאפשר להריץ.","2026-07-26","\u002Fimages\u002Fposts\u002Ffetch-http-errors-and-timeouts.svg","בקשת Fetch מתפצלת לתשובת HTTP עם שגיאה ולכשל רשת או פסק זמן",[143,144,145,146],"JavaScript","Fetch","HTTP","Debugging","fullstack",{"path":149,"title":150,"description":151,"type":152,"level":25,"minutes":26,"date":153,"cover":154,"coverAlt":155,"featured":14,"tags":156,"topic":161},"\u002Fai\u002Frag-answering-from-your-documents","RAG: איך נותנים למודל לענות מתוך המסמכים שלכם","מה מחפשים לפני שמבקשים מהמודל לכתוב, למה קטעי המקור חשובים, ואיך בודקים אם התשובה באמת נשענת עליהם.","הסבר","2026-07-21","\u002Fimages\u002Fposts\u002Frag-answering-from-your-documents.svg","מסמכי מקור נבדקים בזכוכית מגדלת, קטע מתאים נבחר ועובר לכרטיס תשובה",[157,158,159,160],"AI","RAG","חיפוש","מסמכים","ai",{"path":163,"title":164,"description":165,"type":152,"level":25,"minutes":26,"date":166,"cover":167,"coverAlt":168,"featured":14,"tags":169,"topic":20},"\u002Fsmart-home\u002Fmatter-thread-zigbee-explained","Matter, Thread ו־Zigbee: מה ההבדל ומה צריך בבית?","שלושת השמות אינם מתארים את אותו סוג של דבר. הסבר על שכבות התקשורת, תפקידי הרכזת ובדיקת התאמה למערכת קיימת.","2026-07-16","\u002Fimages\u002Fposts\u002Fmatter-thread-zigbee-explained.svg","שתי קבוצות של מכשירי בית חכם, רשת Mesh ורכזת, עם שער שמקשר בין שכבות התקשורת",[170,171,41,172],"Matter","Thread","רשת ביתית",{"path":174,"title":175,"description":176,"type":8,"level":25,"minutes":177,"date":178,"cover":179,"coverAlt":180,"featured":14,"tags":181,"topic":147},"\u002Ffullstack\u002Fbrowser-to-server-save-form","מהדפדפן לשרת: בונים טופס שמירה ורואים מה קורה בדרך","טופס HTML, בקשת POST ושרת Node.js קטן. דוגמה מלאה שממחישה איך נתונים עוברים ואיפה בודקים אותם.",9,"2026-07-11","\u002Fimages\u002Fposts\u002Fbrowser-to-server-save-form.svg","טופס בדפדפן שולח מעטפת נתונים לשרת ומשם למסד הנתונים",[143,182,145,144],"Node.js",{"path":184,"title":185,"description":186,"type":152,"level":25,"minutes":56,"date":187,"cover":188,"coverAlt":189,"featured":190,"tags":191,"topic":161},"\u002Fai\u002Fai-agent-from-request-to-result","סוכן AI: מה קורה בין הבקשה לתוצאה","איך מודל שפה משתמש בכלים, מה ההבדל בין סוכן לתהליך קבוע ואיך מגדירים משימה שאפשר לבדוק.","2026-07-06","\u002Fimages\u002Fposts\u002Fai-agent-from-request-to-result.svg","בקשה עוברת דרך מנגנון בחירה לכלי חיפוש, מסמכים וביצוע, עד לתוצאה שנבדקה",true,[157,192,193,50],"Agents","Tool Calling",{"path":195,"title":196,"description":197,"type":8,"level":25,"minutes":198,"date":199,"cover":200,"coverAlt":201,"featured":14,"tags":202,"topic":20},"\u002Fsmart-home\u002Fsmart-home-first-steps-home-assistant","בית חכם מתחילים בצעד קטן: בחירת ציוד והתקנת Home Assistant","מה צריך לבית חכם ראשון, איך בוחרים בין Home Assistant OS ל-Container, ואיך מתקינים על Raspberry Pi, מחשב, מכונה וירטואלית או NAS.",24,"2026-07-01","\u002Fimages\u002Fposts\u002Fsmart-home-first-steps-home-assistant.svg","בית עם חיישן ותאורה, המחובר לשרת Home Assistant ולשתי אפשרויות התקנה: מערכת הפעלה וקונטיינר",[16,19,203,18,204,50],"התקנה","Raspberry Pi",{"id":206,"title":137,"body":207,"cover":140,"coverAlt":141,"date":139,"description":138,"draft":14,"extension":1428,"featured":14,"level":9,"meta":1429,"minutes":26,"navigation":190,"ogImage":1430,"path":136,"seo":1431,"sitemap":1432,"stem":1433,"tags":1434,"type":8,"updated":1435,"__hash__":1436},"posts\u002Ffullstack\u002Ffetch-http-errors-and-timeouts.md",{"type":208,"value":209,"toc":1419},"minimark",[210,227,249,254,319,339,343,362,365,369,380,387,1020,1043,1046,1050,1053,1115,1118,1153,1163,1166,1252,1256,1259,1347,1350,1354,1357,1366,1373,1377,1415],[211,212,213,214,218,219,222,223,226],"p",{},"הבקשה נמצאת בתוך ",[215,216,217],"code",{},"try",", יש ",[215,220,221],{},"catch"," שמציג הודעת שגיאה, ובכל זאת הקוד ממשיך גם כשהכתובת לא נמצאה. זה קורה כי ",[215,224,225],{},"fetch"," מבדיל בין כישלון בביצוע הבקשה לבין תשובה שקוד הסטטוס שלה מציין שגיאה.",[211,228,229,230,233,234,236,237,240,241,248],{},"אם השרת החזיר ",[215,231,232],{},"404",", התקבלה תשובת HTTP. ה־Promise של ",[215,235,225],{}," יכול להיפתר עם אובייקט ",[215,238,239],{},"Response",", והקוד צריך לבדוק את הסטטוס. MDN מציינת במפורש ש",[242,243,247],"a",{"href":244,"rel":245},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FFetch_API\u002FUsing_Fetch",[246],"nofollow","סטטוס שגיאה כמו 404 אינו גורם כשלעצמו לדחיית ה־Promise",".",[250,251,253],"h2",{"id":252},"ארבעה-מצבים-שנראים-אחרת-בקוד","ארבעה מצבים שנראים אחרת בקוד",[255,256,257,270],"table",{},[258,259,260],"thead",{},[261,262,263,267],"tr",{},[264,265,266],"th",{},"מה קרה",[264,268,269],{},"מה הקוד צריך לבדוק",[271,272,273,293,303,311],"tbody",{},[261,274,275,285],{},[276,277,278,279,281,282],"td",{},"השרת החזיר ",[215,280,232],{}," או ",[215,283,284],{},"500",[276,286,287,281,290],{},[215,288,289],{},"response.ok",[215,291,292],{},"response.status",[261,294,295,298],{},[276,296,297],{},"הבקשה נכשלה בגלל בעיית רשת או מגבלת דפדפן",[276,299,300,301],{},"דחיית ה־Promise ב־",[215,302,221],{},[261,304,305,308],{},[276,306,307],{},"השרת החזיר גוף שאינו JSON צפוי",[276,309,310],{},"סוג התוכן ופענוח הגוף",[261,312,313,316],{},[276,314,315],{},"הקוד ביטל בקשה שהמתינה יותר מדי",[276,317,318],{},"אות הביטול והשגיאה שהתקבלה",[211,320,321,322,324,325,328,329,332,333,338],{},"המאפיין ",[215,323,289],{}," הוא ",[215,326,327],{},"true"," עבור סטטוסים בטווח 200-299. הוא מספר על הסטטוס, לא על תקינות המידע בתוך הגוף. גם תשובת ",[215,330,331],{},"200"," יכולה להכיל JSON שבור או אובייקט שחסרים בו שדות. ",[242,334,337],{"href":335,"rel":336},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FResponse\u002Fok",[246],"תיעוד המאפיין"," מגדיר את הטווח הזה.",[250,340,342],{"id":341},"קריאת-json-היא-פעולה-שיכולה-להיכשל","קריאת JSON היא פעולה שיכולה להיכשל",[211,344,345,346,348,349,352,353,361],{},"אחרי ",[215,347,225],{}," יש בדרך כלל ",[215,350,351],{},"await response.json()",". זו אינה בדיקת הצלחה: הפעולה קוראת את הגוף ומנסה לפענח אותו. אם שרת ביניים החזיר דף HTML, או אם הגוף ריק למרות שציפינו ל־JSON, הפענוח יכול להיכשל. ",[242,354,357,358],{"href":355,"rel":356},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FResponse\u002Fjson",[246],"תיעוד ",[215,359,360],{},"Response.json()"," מפרט את השגיאות האפשריות.",[211,363,364],{},"לכן עדיף לשמור על שני שלבים נפרדים: לבדוק את תשובת HTTP, ואז לקרוא את המידע שהאפליקציה מצפה לקבל. הודעת ״הכתובת לא נמצאה״ שימושית יותר למשתמש מהודעת פענוח שמזכירה תו מתוך דף HTML.",[250,366,368],{"id":367},"פונקציה-קטנה-עם-התנהגות-מוגדרת","פונקציה קטנה עם התנהגות מוגדרת",[211,370,371,372,375,376,379],{},"הפונקציה הבאה מצפה ל־API שמחזיר JSON עם סוג תוכן ",[215,373,374],{},"application\u002Fjson",". היא מטפלת בסטטוס שגיאה, בתשובת ",[215,377,378],{},"204"," ללא תוכן ובהגבלת זמן. היא אינה ספריית בקשות כללית: למשל, היא אינה ממזגת אות ביטול חיצוני ואינה מנסה שוב אוטומטית.",[211,381,382,383,386],{},"כדי לנסות אותה, הפעילו את ",[242,384,385],{"href":174},"השרת מהמאמר הקודם",", פתחו את הדף שהוא מגיש והדביקו את הקוד הבא ב־Console של הדפדפן:",[388,389,394],"pre",{"className":390,"code":391,"language":392,"meta":393,"style":393},"language-javascript shiki shiki-themes github-dark-contrast","class HttpError extends Error {\n  constructor(status, message) {\n    super(message);\n    this.name = \"HttpError\";\n    this.status = status;\n  }\n}\n\nasync function requestJson(url, options = {}, timeoutMs = 8000) {\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n  try {\n    const response = await fetch(url, {\n      ...options,\n      signal: controller.signal\n    });\n\n    const contentType = (response.headers.get(\"content-type\") ?? \"\")\n      .split(\";\")[0].trim().toLowerCase();\n    const isJson = contentType === \"application\u002Fjson\";\n\n    if (!response.ok) {\n      let message = `HTTP ${response.status}`;\n\n      if (isJson) {\n        try {\n          const body = await response.json();\n          if (typeof body?.error?.message === \"string\") {\n            message = body.error.message;\n          }\n        } catch (error) {\n          if (controller.signal.aborted) throw error;\n        }\n      }\n\n      throw new HttpError(response.status, message);\n    }\n\n    if (response.status === 204) return null;\n    if (!isJson) throw new Error(\"השרת החזיר סוג תוכן לא צפוי\");\n\n    return await response.json();\n  } finally {\n    clearTimeout(timer);\n  }\n}\n","javascript","",[215,395,396,419,441,451,470,483,489,494,499,536,556,584,589,597,616,625,631,637,642,675,709,730,735,750,775,780,789,797,818,839,850,856,867,881,887,893,898,911,917,922,945,971,976,990,1001,1010,1015],{"__ignoreMap":393},[397,398,401,405,409,412,415],"span",{"class":399,"line":400},"line",1,[397,402,404],{"class":403},"snl16","class",[397,406,408],{"class":407},"svObZ"," HttpError",[397,410,411],{"class":403}," extends",[397,413,414],{"class":407}," Error",[397,416,418],{"class":417},"s95oV"," {\n",[397,420,422,425,428,432,435,438],{"class":399,"line":421},2,[397,423,424],{"class":403},"  constructor",[397,426,427],{"class":417},"(",[397,429,431],{"class":430},"s9osk","status",[397,433,434],{"class":417},", ",[397,436,437],{"class":430},"message",[397,439,440],{"class":417},") {\n",[397,442,444,448],{"class":399,"line":443},3,[397,445,447],{"class":446},"sDLfK","    super",[397,449,450],{"class":417},"(message);\n",[397,452,454,457,460,463,467],{"class":399,"line":453},4,[397,455,456],{"class":446},"    this",[397,458,459],{"class":417},".name ",[397,461,462],{"class":403},"=",[397,464,466],{"class":465},"sU2Wk"," \"HttpError\"",[397,468,469],{"class":417},";\n",[397,471,473,475,478,480],{"class":399,"line":472},5,[397,474,456],{"class":446},[397,476,477],{"class":417},".status ",[397,479,462],{"class":403},[397,481,482],{"class":417}," status;\n",[397,484,486],{"class":399,"line":485},6,[397,487,488],{"class":417},"  }\n",[397,490,491],{"class":399,"line":56},[397,492,493],{"class":417},"}\n",[397,495,496],{"class":399,"line":26},[397,497,498],{"emptyLinePlaceholder":190},"\n",[397,500,501,504,507,510,512,515,517,520,523,526,529,531,534],{"class":399,"line":177},[397,502,503],{"class":403},"async",[397,505,506],{"class":403}," function",[397,508,509],{"class":407}," requestJson",[397,511,427],{"class":417},[397,513,514],{"class":430},"url",[397,516,434],{"class":417},[397,518,519],{"class":430},"options",[397,521,522],{"class":403}," =",[397,524,525],{"class":417}," {}, ",[397,527,528],{"class":430},"timeoutMs",[397,530,522],{"class":403},[397,532,533],{"class":446}," 8000",[397,535,440],{"class":417},[397,537,539,542,545,547,550,553],{"class":399,"line":538},10,[397,540,541],{"class":403},"  const",[397,543,544],{"class":446}," controller",[397,546,522],{"class":403},[397,548,549],{"class":403}," new",[397,551,552],{"class":407}," AbortController",[397,554,555],{"class":417},"();\n",[397,557,559,561,564,566,569,572,575,578,581],{"class":399,"line":558},11,[397,560,541],{"class":403},[397,562,563],{"class":446}," timer",[397,565,522],{"class":403},[397,567,568],{"class":407}," setTimeout",[397,570,571],{"class":417},"(() ",[397,573,574],{"class":403},"=>",[397,576,577],{"class":417}," controller.",[397,579,580],{"class":407},"abort",[397,582,583],{"class":417},"(), timeoutMs);\n",[397,585,587],{"class":399,"line":586},12,[397,588,498],{"emptyLinePlaceholder":190},[397,590,592,595],{"class":399,"line":591},13,[397,593,594],{"class":403},"  try",[397,596,418],{"class":417},[397,598,599,602,605,607,610,613],{"class":399,"line":10},[397,600,601],{"class":403},"    const",[397,603,604],{"class":446}," response",[397,606,522],{"class":403},[397,608,609],{"class":403}," await",[397,611,612],{"class":407}," fetch",[397,614,615],{"class":417},"(url, {\n",[397,617,619,622],{"class":399,"line":618},15,[397,620,621],{"class":403},"      ...",[397,623,624],{"class":417},"options,\n",[397,626,628],{"class":399,"line":627},16,[397,629,630],{"class":417},"      signal: controller.signal\n",[397,632,634],{"class":399,"line":633},17,[397,635,636],{"class":417},"    });\n",[397,638,640],{"class":399,"line":639},18,[397,641,498],{"emptyLinePlaceholder":190},[397,643,645,647,650,652,655,658,660,663,666,669,672],{"class":399,"line":644},19,[397,646,601],{"class":403},[397,648,649],{"class":446}," contentType",[397,651,522],{"class":403},[397,653,654],{"class":417}," (response.headers.",[397,656,657],{"class":407},"get",[397,659,427],{"class":417},[397,661,662],{"class":465},"\"content-type\"",[397,664,665],{"class":417},") ",[397,667,668],{"class":403},"??",[397,670,671],{"class":465}," \"\"",[397,673,674],{"class":417},")\n",[397,676,678,681,684,686,689,692,695,698,701,704,707],{"class":399,"line":677},20,[397,679,680],{"class":417},"      .",[397,682,683],{"class":407},"split",[397,685,427],{"class":417},[397,687,688],{"class":465},"\";\"",[397,690,691],{"class":417},")[",[397,693,694],{"class":446},"0",[397,696,697],{"class":417},"].",[397,699,700],{"class":407},"trim",[397,702,703],{"class":417},"().",[397,705,706],{"class":407},"toLowerCase",[397,708,555],{"class":417},[397,710,712,714,717,719,722,725,728],{"class":399,"line":711},21,[397,713,601],{"class":403},[397,715,716],{"class":446}," isJson",[397,718,522],{"class":403},[397,720,721],{"class":417}," contentType ",[397,723,724],{"class":403},"===",[397,726,727],{"class":465}," \"application\u002Fjson\"",[397,729,469],{"class":417},[397,731,733],{"class":399,"line":732},22,[397,734,498],{"emptyLinePlaceholder":190},[397,736,738,741,744,747],{"class":399,"line":737},23,[397,739,740],{"class":403},"    if",[397,742,743],{"class":417}," (",[397,745,746],{"class":403},"!",[397,748,749],{"class":417},"response.ok) {\n",[397,751,752,755,758,760,763,766,768,770,773],{"class":399,"line":198},[397,753,754],{"class":403},"      let",[397,756,757],{"class":417}," message ",[397,759,462],{"class":403},[397,761,762],{"class":465}," `HTTP ${",[397,764,765],{"class":417},"response",[397,767,248],{"class":465},[397,769,431],{"class":417},[397,771,772],{"class":465},"}`",[397,774,469],{"class":417},[397,776,778],{"class":399,"line":777},25,[397,779,498],{"emptyLinePlaceholder":190},[397,781,783,786],{"class":399,"line":782},26,[397,784,785],{"class":403},"      if",[397,787,788],{"class":417}," (isJson) {\n",[397,790,792,795],{"class":399,"line":791},27,[397,793,794],{"class":403},"        try",[397,796,418],{"class":417},[397,798,800,803,806,808,810,813,816],{"class":399,"line":799},28,[397,801,802],{"class":403},"          const",[397,804,805],{"class":446}," body",[397,807,522],{"class":403},[397,809,609],{"class":403},[397,811,812],{"class":417}," response.",[397,814,815],{"class":407},"json",[397,817,555],{"class":417},[397,819,821,824,826,829,832,834,837],{"class":399,"line":820},29,[397,822,823],{"class":403},"          if",[397,825,743],{"class":417},[397,827,828],{"class":403},"typeof",[397,830,831],{"class":417}," body?.error?.message ",[397,833,724],{"class":403},[397,835,836],{"class":465}," \"string\"",[397,838,440],{"class":417},[397,840,842,845,847],{"class":399,"line":841},30,[397,843,844],{"class":417},"            message ",[397,846,462],{"class":403},[397,848,849],{"class":417}," body.error.message;\n",[397,851,853],{"class":399,"line":852},31,[397,854,855],{"class":417},"          }\n",[397,857,859,862,864],{"class":399,"line":858},32,[397,860,861],{"class":417},"        } ",[397,863,221],{"class":403},[397,865,866],{"class":417}," (error) {\n",[397,868,870,872,875,878],{"class":399,"line":869},33,[397,871,823],{"class":403},[397,873,874],{"class":417}," (controller.signal.aborted) ",[397,876,877],{"class":403},"throw",[397,879,880],{"class":417}," error;\n",[397,882,884],{"class":399,"line":883},34,[397,885,886],{"class":417},"        }\n",[397,888,890],{"class":399,"line":889},35,[397,891,892],{"class":417},"      }\n",[397,894,896],{"class":399,"line":895},36,[397,897,498],{"emptyLinePlaceholder":190},[397,899,901,904,906,908],{"class":399,"line":900},37,[397,902,903],{"class":403},"      throw",[397,905,549],{"class":403},[397,907,408],{"class":407},[397,909,910],{"class":417},"(response.status, message);\n",[397,912,914],{"class":399,"line":913},38,[397,915,916],{"class":417},"    }\n",[397,918,920],{"class":399,"line":919},39,[397,921,498],{"emptyLinePlaceholder":190},[397,923,925,927,930,932,935,937,940,943],{"class":399,"line":924},40,[397,926,740],{"class":403},[397,928,929],{"class":417}," (response.status ",[397,931,724],{"class":403},[397,933,934],{"class":446}," 204",[397,936,665],{"class":417},[397,938,939],{"class":403},"return",[397,941,942],{"class":446}," null",[397,944,469],{"class":417},[397,946,948,950,952,954,957,959,961,963,965,968],{"class":399,"line":947},41,[397,949,740],{"class":403},[397,951,743],{"class":417},[397,953,746],{"class":403},[397,955,956],{"class":417},"isJson) ",[397,958,877],{"class":403},[397,960,549],{"class":403},[397,962,414],{"class":407},[397,964,427],{"class":417},[397,966,967],{"class":465},"\"השרת החזיר סוג תוכן לא צפוי\"",[397,969,970],{"class":417},");\n",[397,972,974],{"class":399,"line":973},42,[397,975,498],{"emptyLinePlaceholder":190},[397,977,979,982,984,986,988],{"class":399,"line":978},43,[397,980,981],{"class":403},"    return",[397,983,609],{"class":403},[397,985,812],{"class":417},[397,987,815],{"class":407},[397,989,555],{"class":417},[397,991,993,996,999],{"class":399,"line":992},44,[397,994,995],{"class":417},"  } ",[397,997,998],{"class":403},"finally",[397,1000,418],{"class":417},[397,1002,1004,1007],{"class":399,"line":1003},45,[397,1005,1006],{"class":407},"    clearTimeout",[397,1008,1009],{"class":417},"(timer);\n",[397,1011,1013],{"class":399,"line":1012},46,[397,1014,488],{"class":417},[397,1016,1018],{"class":399,"line":1017},47,[397,1019,493],{"class":417},[211,1021,1022,1023,1026,1027,1030,1031,1033,1034,1037,1038,248],{},"ה־",[215,1024,1025],{},"await"," לפני ",[215,1028,1029],{},"response.json()"," חשוב כאן. הוא משאיר את הטיימר פעיל גם בזמן קריאת הגוף, וה־",[215,1032,998],{}," מבטל אותו אחרי סיום הפעולה או כישלונה. ",[215,1035,1036],{},"AbortController"," מאפשר להעביר ל־Fetch אות ביטול; זהו המנגנון המתואר ב",[242,1039,1042],{"href":1040,"rel":1041},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FAbortController",[246],"תיעוד MDN",[211,1044,1045],{},"בפענוח של הודעת שגיאה יש התנהגות מכוונת: אם הגוף אינו JSON תקין, עדיין נשמור את סטטוס HTTP. אם הפענוח נכשל מפני שהבקשה בוטלה, נעביר את שגיאת הביטול במקום להסתיר אותה.",[250,1047,1049],{"id":1048},"מנסים-הצלחה-וכישלון","מנסים הצלחה וכישלון",[211,1051,1052],{},"תחילה בקשו את רשימת הפוסטים:",[388,1054,1056],{"className":390,"code":1055,"language":392,"meta":393,"style":393},"requestJson(\"\u002Fapi\u002Fposts\")\n  .then(data => console.log(data.posts))\n  .catch(error => console.error(error.name, error.message));\n",[215,1057,1058,1070,1095],{"__ignoreMap":393},[397,1059,1060,1063,1065,1068],{"class":399,"line":400},[397,1061,1062],{"class":407},"requestJson",[397,1064,427],{"class":417},[397,1066,1067],{"class":465},"\"\u002Fapi\u002Fposts\"",[397,1069,674],{"class":417},[397,1071,1072,1075,1078,1080,1083,1086,1089,1092],{"class":399,"line":421},[397,1073,1074],{"class":417},"  .",[397,1076,1077],{"class":407},"then",[397,1079,427],{"class":417},[397,1081,1082],{"class":430},"data",[397,1084,1085],{"class":403}," =>",[397,1087,1088],{"class":417}," console.",[397,1090,1091],{"class":407},"log",[397,1093,1094],{"class":417},"(data.posts))\n",[397,1096,1097,1099,1101,1103,1106,1108,1110,1112],{"class":399,"line":443},[397,1098,1074],{"class":417},[397,1100,221],{"class":407},[397,1102,427],{"class":417},[397,1104,1105],{"class":430},"error",[397,1107,1085],{"class":403},[397,1109,1088],{"class":417},[397,1111,1105],{"class":407},[397,1113,1114],{"class":417},"(error.name, error.message));\n",[211,1116,1117],{},"התוצאה היא מערך, גם אם הוא ריק. כעת בקשו כתובת שאינה קיימת:",[388,1119,1121],{"className":390,"code":1120,"language":392,"meta":393,"style":393},"requestJson(\"\u002Fapi\u002Fmissing\")\n  .catch(error => console.log(error.name, error.status));\n",[215,1122,1123,1134],{"__ignoreMap":393},[397,1124,1125,1127,1129,1132],{"class":399,"line":400},[397,1126,1062],{"class":407},[397,1128,427],{"class":417},[397,1130,1131],{"class":465},"\"\u002Fapi\u002Fmissing\"",[397,1133,674],{"class":417},[397,1135,1136,1138,1140,1142,1144,1146,1148,1150],{"class":399,"line":421},[397,1137,1074],{"class":417},[397,1139,221],{"class":407},[397,1141,427],{"class":417},[397,1143,1105],{"class":430},[397,1145,1085],{"class":403},[397,1147,1088],{"class":417},[397,1149,1091],{"class":407},[397,1151,1152],{"class":417},"(error.name, error.status));\n",[211,1154,1155,1156,1159,1160,1162],{},"הפלט הוא ",[215,1157,1158],{},"HttpError 404",". עכשיו הכישלון מגיע ל־",[215,1161,221],{}," משום שהפונקציה שלנו בדקה את התשובה וזרקה שגיאה, לא משום ש־Fetch עושה זאת עבור כל סטטוס שגיאה.",[211,1164,1165],{},"אפשר גם לשלוח פוסט עם כותרת ריקה ולראות שההודעה מהשרת נשמרת:",[388,1167,1169],{"className":390,"code":1168,"language":392,"meta":393,"style":393},"requestJson(\"\u002Fapi\u002Fposts\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application\u002Fjson\" },\n  body: JSON.stringify({ title: \"\" })\n}).catch(error => console.log(error.status, error.message));\n",[215,1170,1171,1182,1193,1210,1232],{"__ignoreMap":393},[397,1172,1173,1175,1177,1179],{"class":399,"line":400},[397,1174,1062],{"class":407},[397,1176,427],{"class":417},[397,1178,1067],{"class":465},[397,1180,1181],{"class":417},", {\n",[397,1183,1184,1187,1190],{"class":399,"line":421},[397,1185,1186],{"class":417},"  method: ",[397,1188,1189],{"class":465},"\"POST\"",[397,1191,1192],{"class":417},",\n",[397,1194,1195,1198,1201,1204,1207],{"class":399,"line":443},[397,1196,1197],{"class":417},"  headers: { ",[397,1199,1200],{"class":465},"\"Content-Type\"",[397,1202,1203],{"class":417},": ",[397,1205,1206],{"class":465},"\"application\u002Fjson\"",[397,1208,1209],{"class":417}," },\n",[397,1211,1212,1215,1218,1220,1223,1226,1229],{"class":399,"line":453},[397,1213,1214],{"class":417},"  body: ",[397,1216,1217],{"class":446},"JSON",[397,1219,248],{"class":417},[397,1221,1222],{"class":407},"stringify",[397,1224,1225],{"class":417},"({ title: ",[397,1227,1228],{"class":465},"\"\"",[397,1230,1231],{"class":417}," })\n",[397,1233,1234,1237,1239,1241,1243,1245,1247,1249],{"class":399,"line":472},[397,1235,1236],{"class":417},"}).",[397,1238,221],{"class":407},[397,1240,427],{"class":417},[397,1242,1105],{"class":430},[397,1244,1085],{"class":403},[397,1246,1088],{"class":417},[397,1248,1091],{"class":407},[397,1250,1251],{"class":417},"(error.status, error.message));\n",[250,1253,1255],{"id":1254},"מה-מציגים-למשתמש","מה מציגים למשתמש?",[211,1257,1258],{},"בממשק כדאי לתרגם את סוג הכישלון להודעה מתאימה. שדה לא תקין דורש תיקון. בעיית חיבור יכולה להצדיק ניסיון נוסף. גוף לא צפוי מעיד על בעיה שדורשת בדיקה בצד המערכת.",[388,1260,1262],{"className":390,"code":1261,"language":392,"meta":393,"style":393},"function messageFor(error) {\n  if (error.name === \"AbortError\") {\n    return \"הבקשה ארכה זמן רב מדי. בדוק אם הפעולה הושלמה לפני ניסיון נוסף.\";\n  }\n\n  if (error instanceof HttpError) return error.message;\n\n  return \"לא הצלחנו לקבל תשובה תקינה. התוכן שלך נשאר בטופס.\";\n}\n",[215,1263,1264,1278,1293,1302,1306,1310,1329,1333,1343],{"__ignoreMap":393},[397,1265,1266,1269,1272,1274,1276],{"class":399,"line":400},[397,1267,1268],{"class":403},"function",[397,1270,1271],{"class":407}," messageFor",[397,1273,427],{"class":417},[397,1275,1105],{"class":430},[397,1277,440],{"class":417},[397,1279,1280,1283,1286,1288,1291],{"class":399,"line":421},[397,1281,1282],{"class":403},"  if",[397,1284,1285],{"class":417}," (error.name ",[397,1287,724],{"class":403},[397,1289,1290],{"class":465}," \"AbortError\"",[397,1292,440],{"class":417},[397,1294,1295,1297,1300],{"class":399,"line":443},[397,1296,981],{"class":403},[397,1298,1299],{"class":465}," \"הבקשה ארכה זמן רב מדי. בדוק אם הפעולה הושלמה לפני ניסיון נוסף.\"",[397,1301,469],{"class":417},[397,1303,1304],{"class":399,"line":453},[397,1305,488],{"class":417},[397,1307,1308],{"class":399,"line":472},[397,1309,498],{"emptyLinePlaceholder":190},[397,1311,1312,1314,1317,1320,1322,1324,1326],{"class":399,"line":485},[397,1313,1282],{"class":403},[397,1315,1316],{"class":417}," (error ",[397,1318,1319],{"class":403},"instanceof",[397,1321,408],{"class":407},[397,1323,665],{"class":417},[397,1325,939],{"class":403},[397,1327,1328],{"class":417}," error.message;\n",[397,1330,1331],{"class":399,"line":56},[397,1332,498],{"emptyLinePlaceholder":190},[397,1334,1335,1338,1341],{"class":399,"line":26},[397,1336,1337],{"class":403},"  return",[397,1339,1340],{"class":465}," \"לא הצלחנו לקבל תשובה תקינה. התוכן שלך נשאר בטופס.\"",[397,1342,469],{"class":417},[397,1344,1345],{"class":399,"line":177},[397,1346,493],{"class":417},[211,1348,1349],{},"המשפט האחרון מתאים רק אם הממשק אכן משאיר את התוכן בטופס. הודעת שגיאה היא חלק מההתנהגות של האפליקציה, ולכן היא צריכה לתאר את מה שקרה בפועל.",[250,1351,1353],{"id":1352},"ביטול-בקשה-אינו-ביטול-הפעולה-בשרת","ביטול בקשה אינו ביטול הפעולה בשרת",[211,1355,1356],{},"יכול להיות שהשרת שמר פוסט, אבל החיבור נקטע לפני שהתשובה הגיעה. מבחינת הדפדפן הבקשה נכשלה; מבחינת השרת היא הושלמה. ביטול ההמתנה בדפדפן אינו מבטיח ביטול של העבודה בשרת.",[211,1358,1359,1360,1365],{},"לכן ניסיון חוזר אוטומטי של פעולה שיוצרת מידע עלול ליצור כפילות. לפני שמוסיפים אותו, צריך להגדיר איך מזהים את אותה פעולה ואיך מונעים ביצוע כפול. MDN מסבירה את ",[242,1361,1364],{"href":1362,"rel":1363},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FGlossary\u002FIdempotent",[246],"מושג האידמפוטנטיות ב־HTTP",": חזרה על בקשה אידמפוטנטית אמורה להשאיר את אותה השפעה מיועדת בשרת.",[211,1367,1368,1369,1372],{},"בדוגמת הפוסטים, כל ",[215,1370,1371],{},"POST"," תקין יוצר רשומה חדשה. זו התנהגות שקל לראות, ולכן אין בה ניסיון חוזר אוטומטי. השלב הבא הוא לתכנן מזהה פעולה או מנגנון אחר שמאפשר לדעת אם אותה שמירה כבר בוצעה.",[250,1374,1376],{"id":1375},"לקריאה-נוספת","לקריאה נוספת",[1378,1379,1380,1387,1396,1402,1410],"ul",{},[1381,1382,1383,248],"li",{},[242,1384,1386],{"href":244,"rel":1385},[246],"MDN: שימוש ב־Fetch",[1381,1388,1389,248],{},[242,1390,1392,1393],{"href":335,"rel":1391},[246],"MDN: המאפיין ",[215,1394,1395],{},"Response.ok",[1381,1397,1398,248],{},[242,1399,1401],{"href":355,"rel":1400},[246],"MDN: פענוח JSON מתשובה",[1381,1403,1404,248],{},[242,1405,1407,1408],{"href":1040,"rel":1406},[246],"MDN: ביטול עם ",[215,1409,1036],{},[1381,1411,1412,248],{},[242,1413,1414],{"href":174},"השרת והטופס ששימשו לניסוי",[1416,1417,1418],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":393,"searchDepth":421,"depth":421,"links":1420},[1421,1422,1423,1424,1425,1426,1427],{"id":252,"depth":421,"text":253},{"id":341,"depth":421,"text":342},{"id":367,"depth":421,"text":368},{"id":1048,"depth":421,"text":1049},{"id":1254,"depth":421,"text":1255},{"id":1352,"depth":421,"text":1353},{"id":1375,"depth":421,"text":1376},"md",{},"\u002Fimages\u002Fposts\u002Ffetch-http-errors-and-timeouts.png",{"title":137,"description":138},{"loc":136},"fullstack\u002Ffetch-http-errors-and-timeouts",[143,144,145,146],null,"uHaJXFbDcUyN_SDpCZw_jWHwexlJ5iqAbG_W-W4p5ZI",1791147941795]