תקלה באמצע התגובה מתחמקת ממנגנון הטיימאאוט המקובל של requests

עודכן לאחרונה ב-August 17, 2026
תקלה באמצע התגובה מתחמקת ממנגנון הטיימאאוט המקובל של requests
סיכום AI
מצאתם באגים בקוד קיים של requests? בדקו הנחות לגבי redirects, handlers שתופסים רק requests.exceptions.Timeout אבל מצפים לכסות תקיעה באמצע body, וצרכני .text שמקבלים תגובות בלי charset. עוברים באופן מכני ל-httpx? עדכנו namespaces של חריגות ל-httpx.TimeoutException או למחלקות הצרות יותר לפי שלב, החליטו אם להפעיל follow_redirects, ובדקו מחדש הנחות פענוח. ה-handler הקיים של requests כבר מפספס את המקרה המוצג של תקיעה באמצע body; המיגרציה לא יוצרת את הבאג הזה. כותבים משהו חדש שמביא הרבה URLs? httpx הוא מועמד טוב כשצריך AsyncClient ו-timeouts נפרדים ל-connect, read, write ו-pool.

כתבו את ה-guard שכולם כותבים:

try:
    r = requests.get(url, timeout=0.5)
except requests.exceptions.Timeout:
    retry()

עכשיו כוונו אותו לשרת ששולח מיד את שורת הסטטוס והכותרות, ואז נתקע לפני גוף התגובה. הקריאה באמת עוברת את מגבלת הזמן. אבל ה-guard לא מופעל. החריגה שנזרקת היא ConnectionError, ו-ConnectionError אינה תת-מחלקה של Timeout.

אותו מצב ב-httpx יזרוק ReadTimeout, שהוא TimeoutException, ולכן ה-guard המקביל כן יתפוס אותו.

יצאתי לבדוק במה httpx שונה מ-requests במקומות ששוברים סקרייפרים, ותכננתי לכתוב על async. בסוף התברר ש-async הוא כמעט הדבר הכי פחות מעניין ברשימה.

מה בדקתי ואיך

שמתי שמונה בדיקות מול שרת fixture מקומי, כי לקוח שמספר מה הוא עשה אינו הוכחה למה שבאמת קרה. השרת סופר חיבורי TCP — מתעדכן פעם אחת לכל socket שהתקבל, עוד לפני שמפענחים שורת בקשה כלשהי — וגם מסלולים שנשלפו בפועל. שימוש חוזר בחיבורים ו-follow redirects הם שניהם טענות לגבי ה-wire, וה-wire הוא המקום שבו בודקים אותן.

httpx 0.28.1 עם התוספת http2, requests 2.34.2, Python 3.14.2, macOS arm64. שניהם הורצו באותו virtualenv טרי, כדי שאף אחד לא יירש עקבות מהשני. פלט גולמי: httpx-probes.json.

שש תחזיות הוזנו למערכת לפני ההרצה הראשונה ונשארו שם גם אחריה. שלוש פגעו, שתיים היו שגויות, ואחת צדקה לגבי המקרה שחשבתי עליו אבל פספסה את המקרה שבאמת חשוב. prediction-scorecard.json כולל את החשבון.

ברירות המחדל שמשתנות מתחת לידיים

Measured results chart: Defaults that differ between clients

התנהגותrequests 2.34.2httpx 0.28.1
עוקב אחרי redirects כברירת מחדלכןלא
קריאה ברמת מודול עושה reuse לחיבוריםלאלא
תקיעה לפני headersReadTimeoutReadTimeout
תקיעה באמצע bodyConnectionErrorReadTimeout
בלי charset מוצהרISO-8859-1utf-8
HTTP/2לא זמיןאופציונלי, עובד
timeout נפרד ל-connect/read/write/poolלאכן

Redirects, שימוש חוזר ב-sockets, תוצאות חריגות, פענוח וקביעת פרוטוקול נצפו בבדיקות. צורת ה-API של ה-timeout והיעדר דגל HTTP/2 ב-requests הם תצפיות על יכולות ה-API. httpx-probes.json.

שלוש מהשורות האלה ישנו את מה שהקוד שלכם עושה ביום שתעברו, בלי להתריע.

Redirects: כבוי כברירת מחדל, והשרת מוכיח את זה

שרשרת redirect בת ארבע קפיצות שמסתיימת ב-/ok:

לקוחמה השרת ראההסטטוס שהוחזר
requests5 בקשות200
httpxבקשה 1302
httpx, follow_redirects=True5 בקשות200

החמש הוא ארבע קפיצות ועוד יעד הסיום. התחזית שלי אמרה ארבע, שזה היה חישוב שלא בדקתי; הכיוון היה הטענה, והמספר תוקן כאן במקום להיטשטש בשקט בטקסט.

זו התנהגות מתועדת של httpx, והיא גם בחירה עיצובית הגיונית — redirect הוא משהו שלפעמים חשוב ללקוח לדעת עליו. אבל זו גם הדרך הכי סבירה שבה מיגרציה נשברת בלי לזרוק שגיאה. הקוד מקבל 302, ה-response.text ריק, הפרסר לא מוצא שורות, והלוגים שלכם אומרים 200 OK… בעצם הם אומרים 302, ואף אחד לא עקב אחרי קוד הסטטוס כי עם requests לא היה שום דבר לעקוב אחריו.

ממצא ה-timeout, שהוא זה שטעיתי בו

צפיתי ש-httpx יזהה במדויק איזה שלב נכשל, ו-requests יטשטש את הכול למחלקה אחת. בפועל זה הפוך.

מקור רשמי: Requests timeout documentation.

System diagram: Where the Timeout Lands

מקור רשמי: HTTPX timeout documentation.

תקיעהrequestshttpx
לפני שורת הסטטוסReadTimeoutReadTimeout
באמצע ה-body, אחרי שליחת headersConnectionErrorReadTimeout

httpx נותן את אותו שם מדויק לשני המצבים. requests מפריד ביניהם — וגם מפריד אותם בדיוק על הגבול ש-code retry נכתב סביבו.

ההשלכה אינה מסקנה מהיררכיית המחלקות. הרצתי את ה-guard:

תקיעהexcept requests.exceptions.Timeoutexcept httpx.TimeoutException
לפני שורת הסטטוסתופסתופס
באמצע ה-bodyבורח כ-ConnectionErrorתופס

timeout-retry-guard.json. requests.exceptions.ConnectionError אינה תת-מחלקה של requests.exceptions.Timeout; לעומת זאת httpx.ReadTimeout היא תת-מחלקה של httpx.TimeoutException.

הודעת החריגה של requests אומרת Read timed out. בתוך ConnectionError. הספרייה כן יודעת מה קרה. היא פשוט לא מספרת זאת לעץ הטיפוסים, וזה בדיוק מה שה-except שלכם בודק.

המקרה שנמדד כאן הוא ספציפי: headers מגיעים, ואז ההתקדמות של ה-body נעצרת מספיק זמן כדי לעבור את ה-read timeout. תגובה שממשיכה לשלוח chunks בתוך חלון הזמן, כולל stream מכוון, יכולה להתנהג אחרת ולא נבדקה כאן.

ניהול חיבורים: כל ההבדל נמצא ב-API של הלקוח

עשר בקשות GET, ארבע דרכים, וסופרים sockets בצד השרת:

איךכמה sockets נפתחו
httpx.get() × 1010
requests.get() × 1010
httpx.Client()1
requests.Session()1

זהה, וחשוב להגיד זאת כי זה בדיוק הידע העממי הנפוץ ביותר על הזוג הזה — ש-httpx עושה pooling ו-requests לא. בפועל אף אחד מהם לא עושה pooling ברמת מודול. שניהם עושים זאת דרך אובייקט הלקוח. אם היום אתם קוראים ל-requests.get() בלולאה, מעבר ל-httpx.get() בלולאה לא ישנה שום דבר ביחס ל-churn של ה-sockets.

System diagram: Pooling Lives in the Client

HTTP/2 הוא מפורש ודורש את התוספת

מול endpoint ציבורי אחד של HTTP/2 שנרשם בארטיפקט:

מקור רשמי: RFC 9113: HTTP/2.

לקוחהפרוטוקול שננשא במשא ומתן
httpx.Client(http2=True)HTTP/2
httpx.Client(http2=False)HTTP/1.1
requestsHTTP/1.1, אין דגל כזה

צריך את התוספת httpx[http2]. הנחתי ש-pip install httpx רגיל יתן לקוח ששותק ועושה negotiate ל-1.1, ובדקתי לפני שכתבתי את זה:

ImportError: Using http2=True, but the 'h2' package is not installed.
Make sure to install httpx using `pip install httpx[http2]`.

הוא זורק כבר בזמן יצירת Client, לפני בקשה אחת, וההודעה גם מציינת איך לתקן. זו הגרסה הטובה של הכשל הזה, ואני טעיתי לגביה (http2-extra-missing.json).

הבדיקה הזו מוכיחה שהפרוטוקול ננשא בהצלחה במשא ומתן על ה-endpoint הזה. היא לא מוכיחה יתרון במהירות סקרייפינג; לא נבדק כאן workload מקביל ב-HTTP/1.1.

תפוקה סדרתית מול מקבילית על fixture

עשרים בקשות אל endpoint שישן 0.3 שניות:

מצבזמן כוללsockets
Sync, Client אחד6.138 שנ׳1
Async, AsyncClient אחד0.357 שנ׳20

ההרצה המקבילה הסתיימה ב-0.357 שניות לעומת 6.138 שניות בהרצה הסדרתית. אבל היא גם פתחה עשרים חיבורים, בעוד הלקוח הסינכרוני השתמש שוב באחד, כך שהניסוי משנה את מודל הביצוע ואת רמת המקביליות בפועל במקום לבודד את מהירות הספרייה.

זו המסגור הכנה של המספר. זו מדידה של מקביליות מול endpoint איטי במכוון, לא מדידה של httpx. כל לקוח עם async שעובד ינחת בערך באותו אזור, ומול endpoint מהיר הפער מתכווץ.

מקרה ה-charset שלא חשבתי לנבא

ניבאתי שתגובה שכותרת ה-charset שלה משקרת — charset=iso-8859-1 על bytes של utf-8 — תייצר אותו mojibake בשתי הספריות. וזה נכון. שתיהן מחזירות Café Ubersetzung â naïve résumé במקום המקור Café Ubersetzung — naïve résumé.

המקרה שלא ניבאתי הוא זה שבאמת חשוב:

תגובהאיך requests מפענחאיך httpx מפענח
charset=utf-8, bytes של utf-8נכוןנכון
charset=iso-8859-1, bytes של utf-8mojibakemojibake
אין charset בכללmojibakeנכון

requests חוזר ל-ISO-8859-1 כשה-header לא מציין כלום, בעוד httpx משתמש כברירת מחדל ב-utf-8. לכן ב-fixture של חוסר charset, הלקוחות הפיקו טקסט מפוענח שונה דרך .text; צרכנים שמשתמשים ב-response.content שומרים על אותם bytes מקוריים.

זיכרון, כי קל למדוד אותו

Peak RSS, ‏/usr/bin/time -l, תהליך טרי אחד לכל תא:

תאrequestshttpx
רק import36.0 MiB30.6 MiB
import ועוד GET אחד35.8 MiB40.7 MiB

אלה snapshots של תהליך אחד, והערך של requests עם GET אחד שנמצא קצת מתחת לערך ה-import-only שלו חושף את רעש הריצה. אי אפשר להסיק מכאן מסקנה כיוונית על זיכרון; היה צריך דגימות חוזרות וטווחים.

מה זה אומר כשבוחרים

מצאתם באגים בקוד קיים של requests? בדקו הנחות לגבי redirects, handlers שתופסים רק requests.exceptions.Timeout אבל מצפים לכסות תקיעה באמצע body, וצרכני .text שמקבלים תגובות בלי charset.

עוברים באופן מכני ל-httpx? עדכנו namespaces של חריגות ל-httpx.TimeoutException או למחלקות הצרות יותר לפי שלב, החליטו אם להפעיל follow_redirects, ובדקו מחדש הנחות פענוח. ה-handler הקיים של requests כבר מפספס את המקרה המוצג של תקיעה באמצע body; המיגרציה לא יוצרת את הבאג הזה.

כותבים משהו חדש שמביא הרבה URLs? httpx הוא מועמד טוב כשצריך AsyncClient ו-timeouts נפרדים ל-connect, read, write ו-pool. השלבים האלה אומרים לכם איפה ההמתנה קרתה — הקמת חיבור, התקדמות ב-body, העלאת הבקשה, או הקצאה מקומית מה-pool — לא למה השרת המרוחק התנהג כך.

כותבים משהו קטן וסינכרוני? requests בסדר גמור, והוא נמצא בכל מקום. הסיבה לעבור היא לא מהירות.

מה שלא תבחרו, השתמשו באובייקט הלקוח ולא בפונקציה ברמת המודול. זה השינוי היחיד ברשימה הזו שהוא ניצחון ברור בשתי הספריות.

איפה API מנוהל משתלב

כל מה שלמעלה הוא שכבת ה-fetch, ושכבת ה-fetch היא החלק הקל. שום דבר מזה לא מריץ JavaScript, שום דבר לא מתמודד עם anti-bot challenge, ושום דבר לא הופך HTML לשורות שרציתם.

הערת מחבר: Thunderbit היא האפשרות המנוהלת שלנו ל-rendering ול-extraction מתוך URL. היא לא נבדקה ב-harness הזה של לקוחות HTTP. כדאי לשקול את הקטגוריה הזו רק כשבעייתכם היא של שליפת עמוד או חילוץ מובנה — ולא סמנטיקה של לקוח HTTP.

אם אתם שולפים דפים רגילים ומנתחים אותם בעצמכם, שני הלקוחות עדיין רלוונטיים. שירות מנוהל הוא החלטה נפרדת של build מול buy, לא הוכחה לבחירה בין הספריות האלה.

לתמונה הרחבה יותר, ה-web scraping API roundup שלנו מסקר את האפשרויות המארחות, וה-open-source scraper pillar את האפשרויות שמתארחות אצלכם.

נסו את Thunderbit לחילוץ נתוני רשת

מסקנה

עבור שכבת fetch חדשה ב-Python שצריכה מקביליות async, timeouts לפי שלב ו-fallback ל-UTF-8, httpx הוא ברירת המחדל שלי תחת המגבלות שנבדקו כאן. requests עדיין בחירה סבירה לקוד סינכרוני ותיק, כשסיכון המיגרציה גדול מהיתרונות האלה. פרוקסים, מדיניות retries, TLS fingerprinting, streaming, uploads ושונות רשת ריאליסטית לא נבדקו, כך שזה לא דירוג אוניברסלי של לקוחות סקרייפינג.

הסיבה להיזהר היא ברירת המחדל של redirects, והיא אכן סכנה אמיתית דווקא משום שזו בחירה עיצובית טובה. explicit עדיף על implicit — עד לרגע שבו מה שהיה implicit היה חלק נושא-משקל בקוד שכבר שחררתם.

לוח הניקוד שנרשם מראש הסתיים בשלוש תחזיות נכונות, שתיים שגויות ותחזית אחת חלקית. התיקון השימושי היה מחלקת החריגה של mid-body; את שאר ההחלטה צריך לגזור מההתנהגות שנצפתה, לא מהנרטיב של לוח הניקוד.

נסו את Thunderbit לחילוץ נתוני רשת Get Started Free

שאלות נפוצות

האם באמת httpx לא עוקב אחרי redirects? לא כברירת מחדל. השרת ספר בקשה אחת לשרשרת של ארבע קפיצות, והתגובה חזרה כ-302. אפשר להעביר follow_redirects=True בכל קריאה, או להגדיר זאת פעם אחת על ה-Client. זה מתועד ומכוון; ובכל זאת זו עדיין הסכנה הסבירה ביותר למיגרציה שקטה, כי הכשל הוא פרסר ריק במקום חריגה.

האם except requests.exceptions.Timeout באמת לא מספיק? לא במקרה של שרת שנתקע אחרי שליחת headers. במקרה כזה נזרק ConnectionError, שאינה תת-מחלקה של Timeout, ולכן ה-guard מפספס אותה — וזה הודגם ישירות ולא הוסק. אם רוצים לתפוס את שניהם, אפשר לתפוס requests.exceptions.RequestException, אבל אז תתפסו גם דברים שאינם טיימאאוטים.

האם httpx מהיר יותר מ-requests? לא באופן משמעותי, כשמדובר בבקשה אחת בכל פעם — זו לא המטרה שלו. ה-17.2× במבחן הזה הוא מדידה של עשרים בקשות מקבילות מול endpoint של 0.3 שנ׳, כלומר מדידה של מקביליות. אם ה-workload שלכם סדרתי, אל תצפו להאצה ובחרו לפי ברירות המחדל.

האם צריך את התוספת http2? רק אם אתם רוצים HTTP/2 — ואם תגדירו http2=True בלעדיה, httpx יזרוק ImportError בזמן יצירת ה-Client, עם הודעה שמבקשת להתקין httpx[http2]. אין כאן downgrade שקט. אני ציפיתי אחרת ובדקתי במקום לכתוב.

מה לא נבדק כאן? התנהגות של פרוקסים, שמשמעותית מאוד בסקרייפינג ודורשת harness משלה. retries — ל-httpx אין לוגיקת retry מובנית, ול-requests יש את זו של urllib3, כך שהשוואה הוגנת היא בעצם בין שתי ספריות retry. TLS fingerprinting, שזה הציר שמערכות anti-bot באמת בוחנות ושאף אחת מהספריות לא פותרת. streaming והעלאות קבצים. וגם זה: הכול כאן רץ על מכונה אחת, גרסת Python אחת, ובשש מתוך שמונה הבדיקות על localhost — מספרי latency משרת fixture הם מדידה של העיצוב, לא של הרשת שלכם.

Ke
Ke
CTO ב-Thunderbit | מדען נתונים בכיר ומומחה ב-ML עם כמעט עשור של ניסיון בלמידת מכונה ובמדע הנתונים, קה שן הוא בוגר אוניברסיטת קולומביה ולשעבר מדען נתונים בכיר ב-Walmart Labs. עם מומחיות עמוקה ומוכרת בקרב עמיתים ב-Python, R, Java וסטטיסטיקה, הוא משתף תובנות מוכחות-בקרב על המעבר מאלגוריתמי AI מורכבים מתיאוריה לארכיטקטורה ברמת production.
תוכן עניינים
Thunderbit · סוכן נתוני אינטרנט מבוסס AI

חלץ נתונים מכל דף בתוך קליק אחד

למעלה מ-250,000 משתמשים סומכים עליו
יש תוכנית חינמית
מדף אינטרנט לגיליון נתונים
תארו מה אתם צריכים — סוכן ה-AI של Thunderbit מחלץ את זה ומייצא ל-Excel, Google Sheets, Airtable או Notion. חינמי להתחלה.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week