Một lỗi kẹt giữa chừng nội dung đã lọt qua bộ xử lý timeout quen thuộc của requests

Cập nhật lần cuối vào August 17, 2026
Một lỗi kẹt giữa chừng nội dung đã lọt qua bộ xử lý timeout quen thuộc của requests
Tóm tắt bằng AI
Finding bugs in existing requests code? Audit redirect assumptions, handlers that catch only requests.exceptions.Timeout but expect to cover a mid-body stall, and .text consumers receiving responses without a charset. Migrating mechanically to httpx? Change exception namespaces to httpx.TimeoutException or the narrower phase classes, decide whether to enable followredirects, and retest decoding assumptions. The existing requests handler already misses the demonstrated mid-body case; migration does not create that particular bug. Writing something new that fetches many URLs? httpx is a candidate when you need AsyncClient and separate connect, read, write, and pool timeouts.

Hãy viết đoạn bảo vệ mà ai cũng viết:

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

Trỏ nó tới một server gửi ngay dòng trạng thái và header, rồi đứng hình trước khi trả body. Lúc này read bị timeout. Nhưng đoạn bảo vệ không hề chạy. Ngoại lệ bật ra là ConnectionError, và ConnectionError không phải là lớp con của Timeout.

Nếu gặp cùng kiểu đứng hình đó với httpx, nó sẽ ném ReadTimeout, mà ReadTimeout lại thuộc TimeoutException, nên guard tương đương sẽ bắt được.

Tôi đi tìm xem httpx khác requests ở những điểm nào có thể làm hỏng scraper, ban đầu nghĩ sẽ viết về async. Cuối cùng thì async lại là thứ ít thú vị nhất trong danh sách.

Tôi đã kiểm tra gì và kiểm tra như thế nào

Tám phép thử trên một server fixture cục bộ, vì client nói rằng nó đã làm gì không phải là bằng chứng cho việc nó thật sự đã làm như vậy. Server đếm TCP connections — tăng một lần cho mỗi socket được chấp nhận, trước khi bất kỳ request line nào được parse — và đếm các path thực sự được fetch. Việc tái sử dụng connection và theo redirect đều là câu chuyện ở tầng wire, nên wire mới là nơi kiểm chứng.

httpx 0.28.1 với extra http2, requests 2.34.2, Python 3.14.2, macOS arm64. Tất cả chạy trong một virtualenv mới để không bên nào “kéo theo” footprint của bên kia. Kết quả thô: httpx-probes.json.

Sáu dự đoán được đưa vào harness trước lần chạy đầu tiên và giữ nguyên sau đó. Ba dự đoán đúng, hai dự đoán sai, một dự đoán đúng với trường hợp tôi nghĩ tới nhưng bỏ lỡ trường hợp quan trọng. prediction-scorecard.json có phần ghi chép chi tiết.

Những mặc định đổi ngầm dưới chân bạn

Biểu đồ kết quả đo: Những mặc định khác nhau giữa các client

Hành virequests 2.34.2httpx 0.28.1
Tự động theo redirect mặc địnhcókhông
Gọi ở mức module có tái sử dụng connectionkhôngkhông
Kẹt trước headersReadTimeoutReadTimeout
Kẹt giữa bodyConnectionErrorReadTimeout
Không khai báo charsetISO-8859-1utf-8
HTTP/2không cócó thể bật, hoạt động
Timeout tách riêng cho connect/read/write/poolkhôngcó

Redirect, tái sử dụng socket, kết quả ngoại lệ, giải mã ký tự và đàm phán giao thức đều được quan sát qua các probe. Hình dạng API của timeout và việc requests không có cờ HTTP/2 là các quan sát về khả năng của API. httpx-probes.json.

Ba dòng trong số này sẽ làm code của bạn đổi hành vi vào ngày bạn chuyển sang, mà không báo trước.

Redirect: tắt mặc định, và server chứng minh điều đó

Một chuỗi redirect 4 chặng, kết thúc ở /ok:

ClientServer thấyStatus trả về
requests5 requests200
httpx1 request302
httpx, follow_redirects=True5 requests200

Số 5 là bốn chặng cộng với đích cuối. Dự đoán của tôi ghi là 4, do tôi tính nhẩm mà không kiểm lại; điều đáng nói là hướng kết luận đúng, còn con số thì được sửa ở đây thay vì lặng lẽ sửa trong văn bản.

Đây là hành vi đã được httpx ghi nhận và cũng là một lựa chọn thiết kế có lý — redirect là thứ người gọi có thể muốn biết. Nhưng nó cũng là cách dễ nhất để migration hỏng mà không nổ lỗi. Code của bạn nhận 302, response.text trống, parser không tìm thấy hàng nào, còn log thì nói 200 OK… thực ra nó sẽ nói 302, chỉ là trước đây với requests chẳng ai buộc phải nhìn status code cả.

Phát hiện về timeout, và đây là chỗ tôi đoán ngược

Tôi đoán httpx sẽ gọi tên đúng phase bị lỗi còn requests sẽ gom cả hai thành một class. Thực tế thì ngược lại.

Tài liệu chính thức: Requests timeout documentation.

Sơ đồ hệ thống: Timeout rơi vào đâu

Tài liệu chính thức: HTTPX timeout documentation.

Trạng thái kẹtrequestshttpx
Trước status lineReadTimeoutReadTimeout
Giữa body, sau khi headers đã gửiConnectionErrorReadTimeout

httpx gọi cùng một tên chính xác cho cả hai trường hợp. requests thì tách chúng ra — và tách qua đúng ranh giới mà code retry đang dựa vào.

Hệ quả không phải là suy luận từ cây thừa kế của class. Tôi đã chạy guard:

Trạng thái kẹtexcept requests.exceptions.Timeoutexcept httpx.TimeoutException
Trước status linebắt đượcbắt được
Giữa bodylọt ra dưới dạng ConnectionErrorbắt được

timeout-retry-guard.json. requests.exceptions.ConnectionError không phải là subclass của requests.exceptions.Timeout; httpx.ReadTimeout là subclass của httpx.TimeoutException.

Thông báo lỗi của requests ghi Read timed out. bên trong ConnectionError. Thư viện biết chuyện gì đã xảy ra. Chỉ là nó không nói điều đó với type system, mà except của bạn lại đang hỏi type system.

Trường hợp đo được ở đây là cụ thể: headers đến trước, rồi dòng body bị dừng đủ lâu để vượt quá read timeout. Một response vẫn tiếp tục đẩy chunk trong cửa sổ timeout, kể cả stream có chủ đích, có thể hành xử khác và không được kiểm tra trong bài này.

Connection pooling: khác biệt nằm trọn trong API client

Mười GET, bốn cách, socket được server đếm:

Cách làmSố socket mở
httpx.get() × 1010
requests.get() × 1010
httpx.Client()1
requests.Session()1

Kết quả y hệt, và đáng nói vì đây là hiểu lầm phổ biến nhất về cặp này — rằng httpx có pool còn requests thì không. Thực ra ở mức module, cả hai đều không pool. Cả hai chỉ pool thông qua object client. Nếu hôm nay bạn đang gọi requests.get() trong vòng lặp, thì chuyển sang httpx.get() trong vòng lặp không thay đổi gì về mức độ churn socket.

Sơ đồ hệ thống: Pool nằm trong client

HTTP/2 phải bật rõ ràng và cần extra

Trên một endpoint HTTP/2 công khai được ghi trong artifact:

Tài liệu chính thức: RFC 9113: HTTP/2.

ClientĐàm phán được
httpx.Client(http2=True)HTTP/2
httpx.Client(http2=False)HTTP/1.1
requestsHTTP/1.1, không có cờ nào

Bạn cần extra httpx[http2]. Tôi tưởng chỉ cần pip install httpx là đủ để client lặng lẽ thương lượng xuống 1.1, và đã đi kiểm tra trước khi viết ra:

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

Nó báo lỗi ngay lúc tạo Client, trước khi gửi bất kỳ request nào, và message còn chỉ luôn cách sửa. Đây là kiểu lỗi “đẹp” của case này, và tôi đã đoán ngược nó (http2-extra-missing.json).

Probe này chỉ chứng minh việc đàm phán giao thức thành công trên endpoint đó. Nó không chứng minh lợi ích về tốc độ scrape; tôi không test một workload HTTP/1.1 tương ứng.

Thông lượng fixture: tuần tự so với đồng thời

Hai mươi request tới một endpoint ngủ 0.3 giây:

Chế độThời gian thực tếSocket
Sync, một Client6.138 s1
Async, một AsyncClient0.357 s20

Lần chạy đồng thời hoàn tất trong 0.357 giây, so với 6.138 giây của lần chạy tuần tự. Nó cũng mở 20 connection trong khi synchronous client chỉ tái sử dụng 1 connection, nên thí nghiệm này đang thay đổi mô hình thực thi và mức độ đồng thời hiệu dụng, chứ không tách riêng tốc độ thư viện.

Đó mới là cách diễn giải trung thực cho con số này. Đây là phép đo concurrency trên một endpoint cố tình chậm, không phải phép đo riêng của httpx. Bất kỳ client nào có async hoạt động tốt đều sẽ rơi vào cùng khu vực này, và với một endpoint nhanh thì khoảng cách sẽ co lại.

Trường hợp charset mà tôi không nghĩ ra để dự đoán

Tôi dự đoán rằng một response có header nói dối — charset=iso-8859-1 trong khi body là bytes utf-8 — sẽ tạo ra mojibake giống hệt ở cả hai. Đúng là như vậy. Cả hai đều trả Café Ubersetzung â naïve résumé trong khi nguồn gốc là Café Ubersetzung — naïve résumé.

Điều tôi không đoán ra là trường hợp quan trọng hơn:

Responserequests giải mãhttpx giải mã
charset=utf-8, bytes utf-8đúngđúng
charset=iso-8859-1, bytes utf-8mojibakemojibake
không có charset nàomojibakeđúng

requests quay về ISO-8859-1 khi header không nói gì, còn httpx mặc định là utf-8. Trong fixture thiếu charset, hai client vì thế cho ra text giải mã khác nhau qua .text; còn người dùng response.content thì vẫn giữ nguyên bytes gốc.

Bộ nhớ, vì đo cái này rất rẻ

Peak RSS, /usr/bin/time -l, mỗi ô là một process mới:

Ô chạyrequestshttpx
Chỉ import36.0 MiB30.6 MiB
Import và thêm một GET35.8 MiB40.7 MiB

Đây là snapshot của từng process riêng lẻ, và giá trị one-GET của requests thấp hơn nhẹ so với import-only cho thấy độ nhiễu của lần chạy. Chúng không đủ để kết luận xu hướng bộ nhớ; cần nhiều mẫu và khoảng giá trị hơn.

Ý nghĩa khi bạn chọn

Đang tìm bug trong code requests hiện có? Hãy rà lại giả định về redirect, các handler chỉ bắt requests.exceptions.Timeout nhưng lại nghĩ là bao phủ được kẹt giữa body, và các consumer .text nhận response không có charset.

Đang migrate cơ học sang httpx? Đổi namespace exception sang httpx.TimeoutException hoặc các class phase hẹp hơn, quyết định có bật follow_redirects hay không, và kiểm tra lại các giả định về decoding. Handler requests hiện tại vốn đã bỏ sót case kẹt giữa body ở trên; migration không tạo ra bug đó.

Đang viết thứ mới để fetch nhiều URL? httpx là ứng viên tốt khi bạn cần AsyncClient và timeout tách riêng cho connect, read, write, và pool. Các phase này cho bạn biết đang chờ ở đâu — thiết lập kết nối, tiến trình đọc body, upload request, hay chờ lấy connection từ pool — chứ không nói vì sao remote host lại cư xử như vậy.

Đang viết thứ nhỏ, đồng bộ? requests vẫn ổn và có mặt ở khắp nơi. Lý do để chuyển không phải là tốc độ.

Dù chọn gì, hãy dùng object client thay vì hàm ở mức module. Đó là thay đổi duy nhất trong danh sách này mà cả hai thư viện đều thắng rõ ràng.

Khi nào nên dùng API được quản lý

Tất cả những gì ở trên đều là tầng fetch, và tầng fetch là phần dễ. Nó không render JavaScript, không xử lý anti-bot challenge, và cũng không biến HTML thành những dòng dữ liệu bạn muốn.

Ghi chú của tác giả: Thunderbit là lựa chọn được quản lý của chúng tôi cho việc render và trích xuất từ URL. Nó không được test trong bộ benchmark HTTP client này. Chỉ nên cân nhắc loại giải pháp này khi vấn đề bạn cần loại bỏ là việc lấy trang hoặc trích xuất có cấu trúc — chứ không phải semantics của HTTP client.

Nếu bạn chỉ fetch trang thông thường rồi tự parse, cả hai client vẫn đều phù hợp. Dịch vụ được quản lý là một quyết định build-versus-buy riêng, không phải bằng chứng để chọn giữa hai thư viện này.

Với bức tranh rộng hơn, bài web scraping API roundup của chúng tôi bao quát các lựa chọn hosted, còn open-source scraper pillar bao quát các lựa chọn tự host.

Dùng thử Thunderbit để trích xuất dữ liệu web

Kết luận

Với một lớp fetch Python mới cần concurrency async, timeout theo từng phase, và fallback UTF-8, httpx là lựa chọn mặc định của tôi trong các ràng buộc đã test ở đây. requests vẫn rất ổn cho code đồng bộ đã trưởng thành, nơi rủi ro migration lớn hơn lợi ích đó. Proxy, retry policy, TLS fingerprinting, streaming, upload, và biến thiên mạng thực tế đều chưa được kiểm tra, nên đây không phải bảng xếp hạng scraper-client cho mọi trường hợp.

Điểm phải cẩn thận nhất là mặc định redirect, và đây là một rủi ro thật sự chính vì nó lại là một quyết định thiết kế tốt. Rõ ràng luôn tốt hơn ngầm định — cho đến khi thứ “ngầm định” đó lại là phần chịu tải trong code bạn đã ship.

Bảng điểm đã đăng ký trước kết thúc với ba dự đoán đúng, hai dự đoán sai, và một dự đoán chưa trọn vẹn. Chỉnh sửa hữu ích nhất là class ngoại lệ của case kẹt giữa body; phần còn lại của quyết định nên đi từ hành vi quan sát được chứ không phải từ câu chuyện quanh scorecard.

Dùng thử Thunderbit để trích xuất dữ liệu web Get Started Free

Câu hỏi thường gặp

httpx thật sự không tự theo redirect à? Đúng, không phải mặc định. Server chỉ đếm 1 request cho chuỗi 4 chặng, và response trả về là 302. Hãy truyền follow_redirects=True ở từng lần gọi, hoặc đặt một lần trên Client. Đây là hành vi được ghi tài liệu và là chủ đích thiết kế; nhưng nó vẫn là thứ dễ hỏng âm thầm nhất khi migration, vì thất bại ở đây là parse ra rỗng chứ không phải exception.

except requests.exceptions.Timeout thật sự chưa đủ sao? Không đủ nếu server đứng hình sau khi gửi headers. Trường hợp đó ném ConnectionError, mà ConnectionError không phải subclass của Timeout, nên guard sẽ bỏ sót — đây là kết quả được chứng minh trực tiếp, không phải suy ra. Hãy bắt requests.exceptions.RequestException nếu bạn muốn bao hết, và chấp nhận rằng bạn cũng sẽ bắt cả những lỗi không phải timeout.

httpx có nhanh hơn requests không? Không đáng kể, nếu chỉ xét từng request đơn lẻ — vì đó không phải mục đích của nó. Con số 17.2× trong bài test này là phép đo 20 request đồng thời trên endpoint 0.3 giây, tức là đo concurrency. Nếu workload của bạn tuần tự, đừng kỳ vọng tăng tốc; hãy chọn dựa trên mặc định phù hợp hơn.

Tôi có cần extra http2 không? Chỉ khi bạn muốn HTTP/2 — và nếu bật http2=True mà không có nó, httpx sẽ ném ImportError ngay lúc tạo Client, kèm thông báo bảo cài httpx[http2]. Không có chuyện âm thầm hạ cấp xuống giao thức thấp hơn. Tôi đã đoán là có và đã đi kiểm tra thay vì viết bừa.

Những gì nào không được test ở đây? Hành vi proxy, vốn cực kỳ quan trọng với scraping và cần bộ harness riêng. Retry — httpx không ship logic retry, còn requests lấy từ urllib3, nên so sánh công bằng thực ra là so sánh hai thư viện retry. TLS fingerprinting, thứ mà anti-bot systems thực sự nhìn vào và cả hai thư viện đều không xử lý. Streaming và file upload. Và toàn bộ bài này đều chạy trên một máy, một phiên bản Python, và localhost cho sáu trong tám probe — các số latency từ server fixture là phép đo thiết kế, không phải mạng của bạn.

Ke
Ke
CTO tại Thunderbit | Chuyên gia Khoa học Dữ liệu cấp cao & ML Với gần một thập kỷ kinh nghiệm trong học máy và khoa học dữ liệu, Ke Shen là cựu sinh viên Đại học Columbia và từng là Chuyên gia Khoa học Dữ liệu cấp cao tại Walmart Labs. Sở hữu chuyên môn sâu về Python, R, Java và Thống kê, được đồng nghiệp công nhận, anh chia sẻ những góc nhìn thực chiến về cách đưa các thuật toán AI phức tạp từ lý thuyết vào kiến trúc sẵn sàng cho môi trường sản xuất.
Mục lục
Thunderbit · Tác nhân dữ liệu web AI

Trích xuất dữ liệu từ bất kỳ trang nào trong 1 lần nhấp

Được hơn 250.000+ người dùng tin tưởng
có gói miễn phí
Từ trang web đến bảng tính
Mô tả điều bạn cần — AI Agent của Thunderbit sẽ scrape và xuất sang Excel, Google Sheets, Airtable hoặc Notion. Bắt đầu miễn phí.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week