การค้างกลางทางที่หลุดรอดจากตัวจัดการ Timeout ของ requests

อัปเดตล่าสุดเมื่อ August 17, 2026
การค้างกลางทางที่หลุดรอดจากตัวจัดการ Timeout ของ requests
สรุปด้วย AI
กำลังไล่บั๊กในโค้ด requests ที่มีอยู่? ตรวจ assumption เรื่อง redirect, handler ที่จับแค่ requests.exceptions.Timeout แต่คิดว่าจะครอบคลุมเคสค้างกลาง body ได้ และผู้ใช้ .text ที่รับ response ไม่มี charset กำลังย้ายไป httpx แบบกลไกตรง ๆ? เปลี่ยน namespace ของ exception ไปใช้ httpx.TimeoutException หรือ class ที่เจาะจงตาม phase, ตัดสินใจว่าจะเปิด follow_redirects หรือไม่ และทดสอบ assumption เรื่อง decoding ใหม่ โค้ด handler ของ requests ที่มีอยู่เดิมก็พลาดเคสค้างกลาง body ที่สาธิตไว้แล้ว การย้ายไม่ได้เป็นตัวสร้างบั๊กเฉพาะกรณีนั้นขึ้นมา กำลังเขียนของใหม่ที่ต้องดึง URL จำนวนมาก? httpx เป็นตัวเลือกที่น่าสนใจเมื่อคุณต้องใช้ AsyncClient และ timeout แยกสำหรับ connect, read, write, และ pool

ลองเขียน guard แบบที่ทุกคนมักใช้กัน:

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

จากนั้นชี้ไปที่เซิร์ฟเวอร์ที่ส่ง status line กับ headers มาให้ทันที แต่กลับค้างก่อนจะส่ง body พอถึงจังหวะอ่านข้อมูลก็ timeout ทว่า guard นี้ไม่ทำงาน สิ่งที่ถูกโยนออกมาคือ ConnectionError และ ConnectionError ก็ไม่ได้สืบทอดมาจาก Timeout

ถ้าเป็นเคสค้างแบบเดียวกันกับ httpx จะได้ ReadTimeout ซึ่งเป็น TimeoutException และ guard แบบเทียบเคียงกันก็ดักได้

ตอนแรกผมตั้งใจจะเขียนเรื่องความต่างระหว่าง httpx กับ requests ที่กระทบงาน scraping โดยคิดว่าจะเน้น async เป็นหลัก สุดท้ายกลับเจอว่า async กลับเป็นเรื่องที่น่าสนใจน้อยที่สุดในลิสต์นี้

สิ่งที่ทดสอบ และวิธีที่ใช้

ผมทดสอบ 8 เคสกับ fixture server บนเครื่อง local เพราะสิ่งที่ client รายงานว่าเกิดขึ้น ไม่ได้แปลว่ามันเกิดขึ้นจริง เซิร์ฟเวอร์จะนับ TCP connections — เพิ่มขึ้นทุกครั้งที่รับ socket ใหม่ ก่อนจะเริ่ม parse request line — และนับ paths ที่ถูกดึงจริง การ reuse connection และการตาม redirect เป็นเรื่องของพฤติกรรมบน wire และ wire นี่แหละคือจุดที่ต้องตรวจสอบ

ใช้ httpx 0.28.1 พร้อม extra http2, requests 2.34.2, Python 3.14.2, บน macOS arm64 ทุกอย่างรันใน virtualenv ใหม่ชุดเดียว เพื่อไม่ให้ตัวหนึ่งติดร่องรอยจากอีกตัว Raw output: httpx-probes.json.

ก่อนรันครั้งแรก ผมใส่คำทำนายไว้ 6 ข้อใน harness และไม่แก้ทีหลัง มี 3 ข้อถูก 2 ข้อผิด และ 1 ข้อถูกแค่กรณีที่ผมนึกถึง แต่พลาดกรณีสำคัญที่สุด ดูบัญชีเต็ม ๆ ได้ใน prediction-scorecard.json

ค่าเริ่มต้นที่เปลี่ยนไปแบบไม่เตือนคุณ

Measured results chart: Defaults that differ between clients

พฤติกรรมrequests 2.34.2httpx 0.28.1
ตาม redirect โดยค่าเริ่มต้นใช่ไม่ใช่
การเรียกในระดับ module reuse connectionsไม่ไม่
ค้างก่อน headersReadTimeoutReadTimeout
ค้างกลาง bodyConnectionErrorReadTimeout
ไม่มี charset ระบุมาISO-8859-1utf-8
HTTP/2ไม่มีให้ใช้ต้องเปิดเพิ่ม และใช้งานได้
แยก timeout สำหรับ connect/read/write/poolไม่ใช่

ผลของ redirect, การ reuse socket, ประเภท exception, การ decode, และการ negotiate โปรโตคอล ถูกสังเกตจาก probe ส่วนรูปแบบ API ของ timeout และการที่ requests ไม่มี flag สำหรับ HTTP/2 เป็นการสังเกตความสามารถของ API ดู httpx-probes.json.

สามแถวในนี้สามารถเปลี่ยนพฤติกรรมโค้ดคุณแบบเงียบ ๆ ได้ในวันที่คุณย้ายไปใช้ตัวใหม่

Redirect: ปิดไว้เป็นค่าเริ่มต้น และเซิร์ฟเวอร์พิสูจน์ให้เห็น

เส้นทาง redirect 4 hop ที่จบที่ /ok:

Clientเซิร์ฟเวอร์เห็นสถานะที่ส่งกลับ
requests5 requests200
httpx1 request302
httpx, follow_redirects=True5 requests200

เลข 5 คือ 4 hop บวกปลายทาง คำทำนายของผมเขียนไว้ว่า 4 ซึ่งเป็นการบวกเลขแบบไม่ได้เช็กเอง ทิศทางของข้อสังเกตนั้นถูก แต่ผมแก้จำนวนให้ตรงตรงนี้แทนที่จะเงียบไว้ในเนื้อหา

พฤติกรรมนี้มีเอกสารทางการของ httpx รองรับ และเป็นแนวคิดที่ปกป้องได้ เพราะ redirect เป็นสิ่งที่ caller อาจอยากรู้ ขณะเดียวกันมันก็เป็นหนึ่งในสาเหตุที่ทำให้ migration พังแบบไม่ส่ง error ได้ง่ายที่สุด โค้ดคุณได้ 302, response.text ว่าง, parser หาแถวไม่เจอ และ log ของคุณบอก 200 OK… หรือจริง ๆ แล้วมันบอก 302 แต่ไม่มีใครดู status code เพราะตอนใช้ requests เดิมทีไม่เคยต้องดู

ผลเรื่อง timeout ที่ผมทายกลับด้าน

ผมเดาว่า httpx จะระบุ phase ที่ล้มเหลวอย่างชัดเจน ส่วน requests จะรวมทุกอย่างไว้ในคลาสเดียว ปรากฏว่าเป็นตรงข้าม

เอกสารอ้างอิงทางการ: Requests timeout documentation.

System diagram: Where the Timeout Lands

เอกสารอ้างอิงทางการ: HTTPX timeout documentation.

การค้างrequestshttpx
ก่อน status lineReadTimeoutReadTimeout
กลาง body หลังส่ง headers แล้วConnectionErrorReadTimeout

httpx ใช้ชื่อเดียวกันได้ถูกต้องทั้งสองกรณี ส่วน requests แยกออกจากกัน — และแยกข้ามเส้นแบ่งที่โค้ด retry มักอ้างอิงอยู่

ผลลัพธ์นี้ไม่ได้สรุปจากลำดับชั้นของ class ผมทดสอบ guard จริง ๆ:

การค้างexcept requests.exceptions.Timeoutexcept httpx.TimeoutException
ก่อน status lineจับได้จับได้
กลาง bodyหลุดออกไปเป็น ConnectionErrorจับได้

timeout-retry-guard.json. requests.exceptions.ConnectionError ไม่ได้สืบทอดจาก requests.exceptions.Timeout; ส่วน httpx.ReadTimeout สืบทอดจาก httpx.TimeoutException.

ข้อความ exception ของ requests จะบอกว่า Read timed out. อยู่ใน ConnectionError ไลบรารีรู้ว่าเกิดอะไรขึ้น แค่ไม่บอก type system และ except ของคุณก็อิงตาม type system นี่แหละ

เคสที่วัดนี้มีเงื่อนไขเฉพาะ: headers มาถึงแล้ว แต่ body หยุดส่งนานพอจะเกิน read timeout response ที่ยังส่ง chunk ต่อเนื่องภายในช่วง timeout รวมถึง stream ที่ตั้งใจให้เป็นแบบนั้น อาจมีพฤติกรรมต่างออกไป และไม่ได้ทดสอบในบทความนี้

การ pool connection: ความต่างทั้งหมดอยู่ที่ client API

GET 10 ครั้ง, 4 วิธี, นับ socket ฝั่งเซิร์ฟเวอร์:

วิธีเปิด socket
httpx.get() × 1010
requests.get() × 1010
httpx.Client()1
requests.Session()1

ผลเหมือนกัน และควรพูดให้ชัด เพราะนี่คือความเข้าใจผิดที่พบบ่อยที่สุดเกี่ยวกับคู่นี้ — คนมักคิดว่า httpx pool ได้ แต่ requests ไม่ได้ ความจริงคือระดับ module ของทั้งคู่ไม่ pool การ pool เกิดผ่าน object แบบ client ถ้าวันนี้คุณเรียก requests.get() ในลูป แล้วเปลี่ยนเป็น httpx.get() ในลูป จำนวน socket churn ก็ไม่ต่างจากเดิม

System diagram: Pooling Lives in the Client

HTTP/2 ต้องเปิดให้ชัด และต้องมี extra

ทดสอบกับ endpoint HTTP/2 สาธารณะหนึ่งตัวที่บันทึกไว้ใน artifact:

เอกสารอ้างอิงทางการ: RFC 9113: HTTP/2.

ClientNegotiate ได้
httpx.Client(http2=True)HTTP/2
httpx.Client(http2=False)HTTP/1.1
requestsHTTP/1.1, ไม่มี flag ให้เปิด

คุณต้องใช้ extra httpx[http2] ผมเดาว่าการติดตั้งแค่ pip install httpx จะได้ client ที่ negotiate กลับไปเป็น 1.1 เงียบ ๆ เลยไปเช็กก่อนจะเขียนลงมา:

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

มัน error ตั้งแต่ตอนสร้าง Client ก่อนจะส่ง request สักครั้ง และข้อความก็ระบุวิธีแก้ชัดเจน นี่คือเวอร์ชันที่ดีของความล้มเหลวแบบนี้ ซึ่งตอนแรกผมเข้าใจกลับด้าน (http2-extra-missing.json)

probe นี้ยืนยันได้ว่า protocol negotiation สำเร็จบน endpoint นั้น แต่ไม่ได้พิสูจน์ว่าเร็วขึ้นสำหรับงาน scraping เพราะไม่ได้ทดสอบ workload HTTP/1.1 ที่จับคู่กันโดยตรง

เปรียบเทียบ throughput แบบต่อเนื่องกับแบบ concurrent

ยี่สิบ requests ไปยัง endpoint ที่ sleep 0.3 วินาที:

โหมดเวลาจริงจำนวน socket
Sync, Client เดียว6.138 s1
Async, AsyncClient เดียว0.357 s20

การรันแบบ concurrent จบใน 0.357 วินาที เทียบกับ 6.138 วินาทีของการรันแบบ sequential แต่มันก็เปิด 20 connections ในขณะที่ client แบบ synchronous reuse เพียง 1 ดังนั้นการทดลองนี้จึงเปลี่ยนทั้ง execution model และระดับ concurrency ที่ใช้จริง มากกว่าจะเป็นการแยกวัดความเร็วของไลบรารีล้วน ๆ

นี่คือการตีความตัวเลขอย่างซื่อสัตย์ มันคือการวัด concurrency กับ endpoint ที่ตั้งใจให้ช้า ไม่ใช่การวัด httpx ไลบรารีใดก็ตามที่รองรับ async ได้ดีก็จะลงอยู่ในโซนเดียวกัน และถ้า endpoint เร็ว ช่องว่างนี้ก็หดลง

เคส charset ที่ผมไม่ได้คิดจะทำนาย

ผมทำนายว่า response ที่ header โกหกว่า charset=iso-8859-1 แต่จริง ๆ เป็น utf-8 bytes จะทำให้ทั้งคู่เพี้ยนเหมือนกัน ซึ่งก็เป็นจริง ทั้งคู่คืนค่าเป็น Café Ubersetzung â naïve résumé ในขณะที่ต้นฉบับเขียนว่า Café Ubersetzung — naïve résumé

แต่เคสที่ผมไม่ทำนาย และมันสำคัญกว่า คือ:

Responserequests decodehttpx decode
charset=utf-8, utf-8 bytesถูกต้องถูกต้อง
charset=iso-8859-1, utf-8 bytesเพี้ยนเพี้ยน
ไม่ระบุ charset เลยเพี้ยนถูกต้อง

requests จะ fallback ไปที่ ISO-8859-1 เมื่อ header ไม่ได้บอกอะไรไว้ ขณะที่ httpx ใช้ utf-8 เป็นค่าเริ่มต้น ดังนั้นใน fixture ที่ไม่มี charset ทั้งคู่จึงให้ข้อความที่ decode ต่างกันเมื่อใช้ .text ส่วนผู้ใช้ที่อ่าน response.content จะยังได้ bytes ดั้งเดิมเหมือนเดิม

เรื่อง memory เพราะวัดได้ง่าย

Peak RSS, /usr/bin/time -l, หนึ่ง process ใหม่ต่อช่อง:

ช่องrequestshttpx
Import อย่างเดียว36.0 MiB30.6 MiB
Import แล้ว GET หนึ่งครั้ง35.8 MiB40.7 MiB

นี่คือ snapshot จากหนึ่ง process และค่าของ requests ที่ต่ำกว่าตอน import-only เล็กน้อยสะท้อน noise ของการรันมากกว่าความหมายเชิงทิศทาง จึงสรุปเรื่อง memory ตามแนวโน้มไม่ได้ ต้องมี sample ซ้ำและช่วงค่าเพิ่มเติม

แล้วทั้งหมดนี้หมายความว่าอย่างไรเวลาจะเลือกใช้

กำลังไล่บั๊กในโค้ด requests ที่มีอยู่? ตรวจ assumption เรื่อง redirect, handler ที่จับแค่ requests.exceptions.Timeout แต่คิดว่าจะครอบคลุมเคสค้างกลาง body ได้ และผู้ใช้ .text ที่รับ response ไม่มี charset

กำลังย้ายไป httpx แบบกลไกตรง ๆ? เปลี่ยน namespace ของ exception ไปใช้ httpx.TimeoutException หรือ class ที่เจาะจงตาม phase, ตัดสินใจว่าจะเปิด follow_redirects หรือไม่ และทดสอบ assumption เรื่อง decoding ใหม่ โค้ด handler ของ requests ที่มีอยู่เดิมก็พลาดเคสค้างกลาง body ที่สาธิตไว้แล้ว การย้ายไม่ได้เป็นตัวสร้างบั๊กเฉพาะกรณีนั้นขึ้นมา

กำลังเขียนของใหม่ที่ต้องดึง URL จำนวนมาก? httpx เป็นตัวเลือกที่น่าสนใจเมื่อคุณต้องใช้ AsyncClient และ timeout แยกสำหรับ connect, read, write, และ pool phase เหล่านี้บอกว่ารออยู่ตรงไหน — ตั้ง connection, ความคืบหน้าของ response body, การอัปโหลด request, หรือการรอ slot ใน local pool — ไม่ได้บอกว่าทำไมเซิร์ฟเวอร์ปลายทางถึงเป็นแบบนั้น

กำลังเขียนของเล็ก ๆ แบบ synchronous? requests ก็ใช้ได้ และพบได้ทุกที่ เหตุผลที่จะย้ายไม่ใช่เรื่องความเร็ว

ไม่ว่าจะเลือกอะไร ให้ใช้ object แบบ client แทนการเรียกฟังก์ชันระดับ module นั่นคือการเปลี่ยนเพียงข้อเดียวในลิสต์นี้ที่ได้ประโยชน์ชัดเจนในทั้งสองไลบรารี

managed API เหมาะตรงไหน

ทุกอย่างด้านบนคือชั้น fetch และชั้น fetch นี่แหละเป็นส่วนที่ง่ายที่สุด ไม่มีส่วนไหน render JavaScript, ไม่มีส่วนไหนจัดการ anti-bot challenge, และไม่มีส่วนไหนแปลง HTML ให้กลายเป็นแถวข้อมูลที่คุณต้องการ

หมายเหตุจากผู้เขียน: Thunderbit คือทางเลือกแบบ managed สำหรับการ render และ extraction จาก URL โดยตรง เราไม่ได้ทดสอบมันใน harness ของ HTTP client นี้ พิจารณาหมวดนี้ก็ต่อเมื่อปัญหาที่คุณอยากแก้คือการดึงหน้าเว็บหรือการ extract แบบมีโครงสร้าง — ไม่ใช่ semantics ของ HTTP client

ถ้าคุณแค่ดึงหน้าเว็บทั่วไปแล้ว parse เอง ทั้งสองตัวก็ยังอยู่ในขอบเขตการใช้งาน แต่ managed service คือการตัดสินใจคนละแบบกับการ build เอง ไม่ใช่หลักฐานว่าต้องเลือกไลบรารีไหน

ถ้าอยากดูภาพรวมของตลาดกว้างกว่านี้ บทสรุป web scraping API roundup ครอบคลุมตัวเลือกแบบ hosted และ open-source scraper pillar ครอบคลุมแบบ self-hosted

ลองใช้ Thunderbit สำหรับดึงข้อมูลเว็บ

บทสรุป

ถ้าคุณกำลังสร้าง fetch layer ใหม่บน Python ที่ต้องการ async concurrency, timeout แยกตาม phase, และ fallback เป็น UTF-8, httpx คือค่าตั้งต้นของผมภายใต้ข้อจำกัดที่ทดสอบครั้งนี้ ส่วน requests ยังเหมาะกับโค้ด synchronous ที่สุกงอมแล้ว ซึ่งความเสี่ยงจากการย้ายมีมากกว่าประโยชน์ที่ได้ โปรดระวังว่าเราไม่ได้ทดสอบ proxies, retry policy, TLS fingerprinting, streaming, uploads, และความแปรผันของเครือข่ายจริง ดังนั้นนี่ไม่ใช่อันดับจัดเรียง scraper client แบบครอบจักรวาล

เหตุผลหลักที่ต้องระวังคือค่าเริ่มต้นเรื่อง redirect และมันเป็นความเสี่ยงจริง ๆ ก็เพราะมันเป็นการตัดสินใจด้านการออกแบบที่ดีอยู่แล้ว สิ่งที่ implicit มักดีกว่า explicit จนกระทั่งมันกลายเป็นสิ่งสำคัญของโค้ดที่คุณปล่อยใช้งานไปแล้ว

scorecard ที่ลงทะเบียนไว้ล่วงหน้าจบด้วยคำทำนายถูก 3 ข้อ ผิด 2 ข้อ และอีก 1 ข้อที่ยังไม่ครบ สิ่งที่ควรเอาไปใช้จริงคือการแก้ความเข้าใจผิดเรื่อง exception กลาง body ส่วนที่เหลือควรตัดสินจากพฤติกรรมที่สังเกตได้ มากกว่าจะยึด narrative ของ scorecard

ลองใช้ Thunderbit สำหรับดึงข้อมูลเว็บ Get Started Free

คำถามที่พบบ่อย

httpx ไม่ตาม redirect จริงหรือ? จริง ถ้าไม่เปิดเอง โดยค่าเริ่มต้นมันจะส่ง request แค่ครั้งเดียวสำหรับ chain 4 hop และ response จะกลับมาเป็น 302 ต้องใส่ follow_redirects=True ในแต่ละครั้ง หรือกำหนดครั้งเดียวที่ Client พฤติกรรมนี้มีเอกสารและตั้งใจออกแบบไว้แล้ว แต่ก็ยังเป็นจุดที่มีโอกาสพังเงียบที่สุดตอน migrate เพราะความล้มเหลวจะออกมาเป็นผล parse ว่าง ๆ ไม่ใช่ exception

except requests.exceptions.Timeout ยังไม่พอจริงหรือ? ไม่พอสำหรับเซิร์ฟเวอร์ที่ค้างหลังส่ง headers กรณีนั้นจะโยน ConnectionError ซึ่งไม่ใช่ subclass ของ Timeout ดังนั้น guard จึงพลาด — ตรงนี้พิสูจน์จากการทดลองจริง ไม่ใช่การอนุมาน ถ้าต้องการจับทั้งคู่ ให้ใช้ requests.exceptions.RequestException แต่ก็ต้องยอมรับว่าคุณจะจับอย่างอื่นที่ไม่ใช่ timeout ไปด้วย

httpx เร็วกว่า requests ไหม? ไม่ได้ต่างกันแบบมีนัยสำคัญสำหรับการยิงทีละ request นั่นไม่ใช่เป้าหมายของมัน ตัวเลข 17.2× ในการทดสอบนี้คือผลของ 20 requests แบบ concurrent ไปยัง endpoint ที่หน่วง 0.3 วินาที ซึ่งเป็นการวัด concurrency ถ้างานของคุณเป็นแบบ sequential ก็อย่าคาดหวังความเร็วเพิ่ม แล้วเลือกจากค่าเริ่มต้นและพฤติกรรมแทน

ต้องใช้ extra http2 ไหม? เฉพาะตอนที่คุณต้องการ HTTP/2 — และถ้าตั้ง http2=True โดยไม่มีมัน httpx จะ raise ImportError ตอนสร้าง Client พร้อมข้อความบอกให้ติดตั้ง httpx[http2] ไม่มีการ downgrade เงียบ ๆ ให้กังวล ผมตอนแรกคิดว่าจะเป็นแบบนั้น แต่ไปเช็กจริงก่อนเขียน

อะไรที่ไม่ได้ทดสอบในบทความนี้? พฤติกรรมของ proxy ซึ่งสำคัญมากสำหรับงาน scraping และควรมี harness แยกต่างหาก การ retry — httpx ไม่มี retry logic มาให้ ส่วน requests ได้มาจาก urllib3 ดังนั้นถ้าจะเทียบกันอย่างแฟร์ จริง ๆ ต้องเทียบ library สำหรับ retry สองตัวด้วยกัน การ fingerprint TLS ซึ่งเป็นแกนที่ระบบ anti-bot ดูจริง และทั้งสองไลบรารีไม่ได้แก้ให้คุณ Streaming และการอัปโหลดไฟล์ รวมถึงทุกอย่างในนี้รันบนเครื่องเดียว Python เวอร์ชันเดียว และใช้ localhost ใน 6 จาก 8 probes ดังนั้นตัวเลข latency จาก fixture server คือการวัดการออกแบบ ไม่ใช่การวัดเครือข่ายของคุณ

Ke
Ke
CTO ที่ Thunderbit | นักวิทยาศาสตร์ข้อมูลอาวุโสและผู้เชี่ยวชาญด้านแมชชีนเลิร์นนิง ด้วยประสบการณ์เกือบสิบปีในด้านแมชชีนเลิร์นนิงและวิทยาศาสตร์ข้อมูล เคเฉินเป็นศิษย์เก่ามหาวิทยาลัยโคลัมเบีย และอดีตนักวิทยาศาสตร์ข้อมูลอาวุโสที่ Walmart Labs ด้วยความเชี่ยวชาญลึกซึ้งที่ได้รับการยอมรับจากเพื่อนร่วมสายงานใน Python, R, Java และสถิติ เขาจึงแบ่งปันมุมมองที่ผ่านการพิสูจน์มาแล้วในการพัฒนาอัลกอริทึม AI ที่ซับซ้อนจากแนวคิดไปสู่สถาปัตยกรรมระดับใช้งานจริง
สารบัญ
Thunderbit · เอเจนต์ข้อมูลเว็บด้วย AI

ดึงข้อมูลจากทุกหน้าได้ใน 1 คลิก

ได้รับความไว้วางใจจากผู้ใช้กว่า 250,000+ คน
มีแผนใช้ฟรี
จากหน้าเว็บสู่สเปรดชีต
บอกสิ่งที่ต้องการ — Thunderbit’s AI Agent จะดึงข้อมูลและส่งออกไปยัง Excel, Google Sheets, Airtable หรือ Notion เริ่มต้นได้ฟรี
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week