curl_httpclient.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. #
  2. # Copyright 2009 Facebook
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. """Non-blocking HTTP client implementation using pycurl."""
  16. import collections
  17. import functools
  18. import logging
  19. import pycurl
  20. import re
  21. import threading
  22. import time
  23. from io import BytesIO
  24. from tornado import httputil
  25. from tornado import ioloop
  26. from tornado.escape import utf8, native_str
  27. from tornado.httpclient import (
  28. HTTPRequest,
  29. HTTPResponse,
  30. HTTPError,
  31. AsyncHTTPClient,
  32. main,
  33. )
  34. from tornado.log import app_log
  35. from typing import Dict, Any, Callable, Union, Optional
  36. import typing
  37. if typing.TYPE_CHECKING:
  38. from typing import Deque, Tuple # noqa: F401
  39. curl_log = logging.getLogger("tornado.curl_httpclient")
  40. CR_OR_LF_RE = re.compile(b"\r|\n")
  41. class CurlAsyncHTTPClient(AsyncHTTPClient):
  42. def initialize( # type: ignore
  43. self, max_clients: int = 10, defaults: Optional[Dict[str, Any]] = None
  44. ) -> None:
  45. super().initialize(defaults=defaults)
  46. # Typeshed is incomplete for CurlMulti, so just use Any for now.
  47. self._multi = pycurl.CurlMulti() # type: Any
  48. self._multi.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout)
  49. self._multi.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket)
  50. self._curls = [self._curl_create() for i in range(max_clients)]
  51. self._free_list = self._curls[:]
  52. self._requests = (
  53. collections.deque()
  54. ) # type: Deque[Tuple[HTTPRequest, Callable[[HTTPResponse], None], float]]
  55. self._fds = {} # type: Dict[int, int]
  56. self._timeout = None # type: Optional[object]
  57. # libcurl has bugs that sometimes cause it to not report all
  58. # relevant file descriptors and timeouts to TIMERFUNCTION/
  59. # SOCKETFUNCTION. Mitigate the effects of such bugs by
  60. # forcing a periodic scan of all active requests.
  61. self._force_timeout_callback = ioloop.PeriodicCallback(
  62. self._handle_force_timeout, 1000
  63. )
  64. self._force_timeout_callback.start()
  65. # Work around a bug in libcurl 7.29.0: Some fields in the curl
  66. # multi object are initialized lazily, and its destructor will
  67. # segfault if it is destroyed without having been used. Add
  68. # and remove a dummy handle to make sure everything is
  69. # initialized.
  70. dummy_curl_handle = pycurl.Curl()
  71. self._multi.add_handle(dummy_curl_handle)
  72. self._multi.remove_handle(dummy_curl_handle)
  73. def close(self) -> None:
  74. self._force_timeout_callback.stop()
  75. if self._timeout is not None:
  76. self.io_loop.remove_timeout(self._timeout)
  77. for curl in self._curls:
  78. curl.close()
  79. self._multi.close()
  80. super().close()
  81. # Set below properties to None to reduce the reference count of current
  82. # instance, because those properties hold some methods of current
  83. # instance that will case circular reference.
  84. self._force_timeout_callback = None # type: ignore
  85. self._multi = None
  86. def fetch_impl(
  87. self, request: HTTPRequest, callback: Callable[[HTTPResponse], None]
  88. ) -> None:
  89. self._requests.append((request, callback, self.io_loop.time()))
  90. self._process_queue()
  91. self._set_timeout(0)
  92. def _handle_socket(self, event: int, fd: int, multi: Any, data: bytes) -> None:
  93. """Called by libcurl when it wants to change the file descriptors
  94. it cares about.
  95. """
  96. event_map = {
  97. pycurl.POLL_NONE: ioloop.IOLoop.NONE,
  98. pycurl.POLL_IN: ioloop.IOLoop.READ,
  99. pycurl.POLL_OUT: ioloop.IOLoop.WRITE,
  100. pycurl.POLL_INOUT: ioloop.IOLoop.READ | ioloop.IOLoop.WRITE,
  101. }
  102. if event == pycurl.POLL_REMOVE:
  103. if fd in self._fds:
  104. self.io_loop.remove_handler(fd)
  105. del self._fds[fd]
  106. else:
  107. ioloop_event = event_map[event]
  108. # libcurl sometimes closes a socket and then opens a new
  109. # one using the same FD without giving us a POLL_NONE in
  110. # between. This is a problem with the epoll IOLoop,
  111. # because the kernel can tell when a socket is closed and
  112. # removes it from the epoll automatically, causing future
  113. # update_handler calls to fail. Since we can't tell when
  114. # this has happened, always use remove and re-add
  115. # instead of update.
  116. if fd in self._fds:
  117. self.io_loop.remove_handler(fd)
  118. self.io_loop.add_handler(fd, self._handle_events, ioloop_event)
  119. self._fds[fd] = ioloop_event
  120. def _set_timeout(self, msecs: int) -> None:
  121. """Called by libcurl to schedule a timeout."""
  122. if self._timeout is not None:
  123. self.io_loop.remove_timeout(self._timeout)
  124. self._timeout = self.io_loop.add_timeout(
  125. self.io_loop.time() + msecs / 1000.0, self._handle_timeout
  126. )
  127. def _handle_events(self, fd: int, events: int) -> None:
  128. """Called by IOLoop when there is activity on one of our
  129. file descriptors.
  130. """
  131. action = 0
  132. if events & ioloop.IOLoop.READ:
  133. action |= pycurl.CSELECT_IN
  134. if events & ioloop.IOLoop.WRITE:
  135. action |= pycurl.CSELECT_OUT
  136. while True:
  137. try:
  138. ret, num_handles = self._multi.socket_action(fd, action)
  139. except pycurl.error as e:
  140. ret = e.args[0]
  141. if ret != pycurl.E_CALL_MULTI_PERFORM:
  142. break
  143. self._finish_pending_requests()
  144. def _handle_timeout(self) -> None:
  145. """Called by IOLoop when the requested timeout has passed."""
  146. self._timeout = None
  147. while True:
  148. try:
  149. ret, num_handles = self._multi.socket_action(pycurl.SOCKET_TIMEOUT, 0)
  150. except pycurl.error as e:
  151. ret = e.args[0]
  152. if ret != pycurl.E_CALL_MULTI_PERFORM:
  153. break
  154. self._finish_pending_requests()
  155. # In theory, we shouldn't have to do this because curl will
  156. # call _set_timeout whenever the timeout changes. However,
  157. # sometimes after _handle_timeout we will need to reschedule
  158. # immediately even though nothing has changed from curl's
  159. # perspective. This is because when socket_action is
  160. # called with SOCKET_TIMEOUT, libcurl decides internally which
  161. # timeouts need to be processed by using a monotonic clock
  162. # (where available) while tornado uses python's time.time()
  163. # to decide when timeouts have occurred. When those clocks
  164. # disagree on elapsed time (as they will whenever there is an
  165. # NTP adjustment), tornado might call _handle_timeout before
  166. # libcurl is ready. After each timeout, resync the scheduled
  167. # timeout with libcurl's current state.
  168. new_timeout = self._multi.timeout()
  169. if new_timeout >= 0:
  170. self._set_timeout(new_timeout)
  171. def _handle_force_timeout(self) -> None:
  172. """Called by IOLoop periodically to ask libcurl to process any
  173. events it may have forgotten about.
  174. """
  175. while True:
  176. try:
  177. ret, num_handles = self._multi.socket_all()
  178. except pycurl.error as e:
  179. ret = e.args[0]
  180. if ret != pycurl.E_CALL_MULTI_PERFORM:
  181. break
  182. self._finish_pending_requests()
  183. def _finish_pending_requests(self) -> None:
  184. """Process any requests that were completed by the last
  185. call to multi.socket_action.
  186. """
  187. while True:
  188. num_q, ok_list, err_list = self._multi.info_read()
  189. for curl in ok_list:
  190. self._finish(curl)
  191. for curl, errnum, errmsg in err_list:
  192. self._finish(curl, errnum, errmsg)
  193. if num_q == 0:
  194. break
  195. self._process_queue()
  196. def _process_queue(self) -> None:
  197. while True:
  198. started = 0
  199. while self._free_list and self._requests:
  200. started += 1
  201. curl = self._free_list.pop()
  202. (request, callback, queue_start_time) = self._requests.popleft()
  203. # TODO: Don't smuggle extra data on an attribute of the Curl object.
  204. curl.info = { # type: ignore
  205. "headers": httputil.HTTPHeaders(),
  206. "buffer": BytesIO(),
  207. "request": request,
  208. "callback": callback,
  209. "queue_start_time": queue_start_time,
  210. "curl_start_time": time.time(),
  211. "curl_start_ioloop_time": self.io_loop.current().time(), # type: ignore
  212. }
  213. try:
  214. self._curl_setup_request(
  215. curl,
  216. request,
  217. curl.info["buffer"], # type: ignore
  218. curl.info["headers"], # type: ignore
  219. )
  220. except Exception as e:
  221. # If there was an error in setup, pass it on
  222. # to the callback. Note that allowing the
  223. # error to escape here will appear to work
  224. # most of the time since we are still in the
  225. # caller's original stack frame, but when
  226. # _process_queue() is called from
  227. # _finish_pending_requests the exceptions have
  228. # nowhere to go.
  229. self._free_list.append(curl)
  230. callback(HTTPResponse(request=request, code=599, error=e))
  231. else:
  232. self._multi.add_handle(curl)
  233. if not started:
  234. break
  235. def _finish(
  236. self,
  237. curl: pycurl.Curl,
  238. curl_error: Optional[int] = None,
  239. curl_message: Optional[str] = None,
  240. ) -> None:
  241. info = curl.info # type: ignore
  242. curl.info = None # type: ignore
  243. self._multi.remove_handle(curl)
  244. self._free_list.append(curl)
  245. buffer = info["buffer"]
  246. if curl_error:
  247. assert curl_message is not None
  248. error = CurlError(curl_error, curl_message) # type: Optional[CurlError]
  249. assert error is not None
  250. code = error.code
  251. effective_url = None
  252. buffer.close()
  253. buffer = None
  254. else:
  255. error = None
  256. code = curl.getinfo(pycurl.HTTP_CODE)
  257. effective_url = curl.getinfo(pycurl.EFFECTIVE_URL)
  258. buffer.seek(0)
  259. # the various curl timings are documented at
  260. # http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html
  261. time_info = dict(
  262. queue=info["curl_start_ioloop_time"] - info["queue_start_time"],
  263. namelookup=curl.getinfo(pycurl.NAMELOOKUP_TIME),
  264. connect=curl.getinfo(pycurl.CONNECT_TIME),
  265. appconnect=curl.getinfo(pycurl.APPCONNECT_TIME),
  266. pretransfer=curl.getinfo(pycurl.PRETRANSFER_TIME),
  267. starttransfer=curl.getinfo(pycurl.STARTTRANSFER_TIME),
  268. total=curl.getinfo(pycurl.TOTAL_TIME),
  269. redirect=curl.getinfo(pycurl.REDIRECT_TIME),
  270. )
  271. try:
  272. info["callback"](
  273. HTTPResponse(
  274. request=info["request"],
  275. code=code,
  276. headers=info["headers"],
  277. buffer=buffer,
  278. effective_url=effective_url,
  279. error=error,
  280. reason=info["headers"].get("X-Http-Reason", None),
  281. request_time=self.io_loop.time() - info["curl_start_ioloop_time"],
  282. start_time=info["curl_start_time"],
  283. time_info=time_info,
  284. )
  285. )
  286. except Exception:
  287. self.handle_callback_exception(info["callback"])
  288. def handle_callback_exception(self, callback: Any) -> None:
  289. app_log.error("Exception in callback %r", callback, exc_info=True)
  290. def _curl_create(self) -> pycurl.Curl:
  291. curl = pycurl.Curl()
  292. if curl_log.isEnabledFor(logging.DEBUG):
  293. curl.setopt(pycurl.VERBOSE, 1)
  294. curl.setopt(pycurl.DEBUGFUNCTION, self._curl_debug)
  295. if hasattr(
  296. pycurl, "PROTOCOLS"
  297. ): # PROTOCOLS first appeared in pycurl 7.19.5 (2014-07-12)
  298. curl.setopt(pycurl.PROTOCOLS, pycurl.PROTO_HTTP | pycurl.PROTO_HTTPS)
  299. curl.setopt(pycurl.REDIR_PROTOCOLS, pycurl.PROTO_HTTP | pycurl.PROTO_HTTPS)
  300. return curl
  301. def _curl_setup_request(
  302. self,
  303. curl: pycurl.Curl,
  304. request: HTTPRequest,
  305. buffer: BytesIO,
  306. headers: httputil.HTTPHeaders,
  307. ) -> None:
  308. curl.setopt(pycurl.URL, native_str(request.url))
  309. # libcurl's magic "Expect: 100-continue" behavior causes delays
  310. # with servers that don't support it (which include, among others,
  311. # Google's OpenID endpoint). Additionally, this behavior has
  312. # a bug in conjunction with the curl_multi_socket_action API
  313. # (https://sourceforge.net/tracker/?func=detail&atid=100976&aid=3039744&group_id=976),
  314. # which increases the delays. It's more trouble than it's worth,
  315. # so just turn off the feature (yes, setting Expect: to an empty
  316. # value is the official way to disable this)
  317. if "Expect" not in request.headers:
  318. request.headers["Expect"] = ""
  319. # libcurl adds Pragma: no-cache by default; disable that too
  320. if "Pragma" not in request.headers:
  321. request.headers["Pragma"] = ""
  322. encoded_headers = [
  323. b"%s: %s"
  324. % (native_str(k).encode("ASCII"), native_str(v).encode("ISO8859-1"))
  325. for k, v in request.headers.get_all()
  326. ]
  327. for line in encoded_headers:
  328. if CR_OR_LF_RE.search(line):
  329. raise ValueError("Illegal characters in header (CR or LF): %r" % line)
  330. curl.setopt(pycurl.HTTPHEADER, encoded_headers)
  331. curl.setopt(
  332. pycurl.HEADERFUNCTION,
  333. functools.partial(
  334. self._curl_header_callback, headers, request.header_callback
  335. ),
  336. )
  337. if request.streaming_callback:
  338. def write_function(b: Union[bytes, bytearray]) -> int:
  339. assert request.streaming_callback is not None
  340. self.io_loop.add_callback(request.streaming_callback, b)
  341. return len(b)
  342. else:
  343. write_function = buffer.write # type: ignore
  344. curl.setopt(pycurl.WRITEFUNCTION, write_function)
  345. curl.setopt(pycurl.FOLLOWLOCATION, request.follow_redirects)
  346. curl.setopt(pycurl.MAXREDIRS, request.max_redirects)
  347. assert request.connect_timeout is not None
  348. curl.setopt(pycurl.CONNECTTIMEOUT_MS, int(1000 * request.connect_timeout))
  349. assert request.request_timeout is not None
  350. curl.setopt(pycurl.TIMEOUT_MS, int(1000 * request.request_timeout))
  351. if request.user_agent:
  352. curl.setopt(pycurl.USERAGENT, native_str(request.user_agent))
  353. else:
  354. curl.setopt(pycurl.USERAGENT, "Mozilla/5.0 (compatible; pycurl)")
  355. if request.network_interface:
  356. curl.setopt(pycurl.INTERFACE, request.network_interface)
  357. if request.decompress_response:
  358. curl.setopt(pycurl.ENCODING, "gzip,deflate")
  359. else:
  360. curl.setopt(pycurl.ENCODING, None)
  361. if request.proxy_host and request.proxy_port:
  362. curl.setopt(pycurl.PROXY, request.proxy_host)
  363. curl.setopt(pycurl.PROXYPORT, request.proxy_port)
  364. if request.proxy_username:
  365. assert request.proxy_password is not None
  366. credentials = httputil.encode_username_password(
  367. request.proxy_username, request.proxy_password
  368. )
  369. curl.setopt(pycurl.PROXYUSERPWD, credentials)
  370. if request.proxy_auth_mode is None or request.proxy_auth_mode == "basic":
  371. curl.setopt(pycurl.PROXYAUTH, pycurl.HTTPAUTH_BASIC)
  372. elif request.proxy_auth_mode == "digest":
  373. curl.setopt(pycurl.PROXYAUTH, pycurl.HTTPAUTH_DIGEST)
  374. else:
  375. raise ValueError(
  376. "Unsupported proxy_auth_mode %s" % request.proxy_auth_mode
  377. )
  378. else:
  379. try:
  380. curl.unsetopt(pycurl.PROXY)
  381. except TypeError: # not supported, disable proxy
  382. curl.setopt(pycurl.PROXY, "")
  383. curl.unsetopt(pycurl.PROXYUSERPWD)
  384. if request.validate_cert:
  385. curl.setopt(pycurl.SSL_VERIFYPEER, 1)
  386. curl.setopt(pycurl.SSL_VERIFYHOST, 2)
  387. else:
  388. curl.setopt(pycurl.SSL_VERIFYPEER, 0)
  389. curl.setopt(pycurl.SSL_VERIFYHOST, 0)
  390. if request.ca_certs is not None:
  391. curl.setopt(pycurl.CAINFO, request.ca_certs)
  392. else:
  393. # There is no way to restore pycurl.CAINFO to its default value
  394. # (Using unsetopt makes it reject all certificates).
  395. # I don't see any way to read the default value from python so it
  396. # can be restored later. We'll have to just leave CAINFO untouched
  397. # if no ca_certs file was specified, and require that if any
  398. # request uses a custom ca_certs file, they all must.
  399. pass
  400. if request.allow_ipv6 is False:
  401. # Curl behaves reasonably when DNS resolution gives an ipv6 address
  402. # that we can't reach, so allow ipv6 unless the user asks to disable.
  403. curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
  404. else:
  405. curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_WHATEVER)
  406. # Set the request method through curl's irritating interface which makes
  407. # up names for almost every single method
  408. curl_options = {
  409. "GET": pycurl.HTTPGET,
  410. "POST": pycurl.POST,
  411. "PUT": pycurl.UPLOAD,
  412. "HEAD": pycurl.NOBODY,
  413. }
  414. custom_methods = {"DELETE", "OPTIONS", "PATCH"}
  415. for o in curl_options.values():
  416. curl.setopt(o, False)
  417. if request.method in curl_options:
  418. curl.unsetopt(pycurl.CUSTOMREQUEST)
  419. curl.setopt(curl_options[request.method], True)
  420. elif request.allow_nonstandard_methods or request.method in custom_methods:
  421. curl.setopt(pycurl.CUSTOMREQUEST, request.method)
  422. else:
  423. raise KeyError("unknown method " + request.method)
  424. body_expected = request.method in ("POST", "PATCH", "PUT")
  425. body_present = request.body is not None
  426. if not request.allow_nonstandard_methods:
  427. # Some HTTP methods nearly always have bodies while others
  428. # almost never do. Fail in this case unless the user has
  429. # opted out of sanity checks with allow_nonstandard_methods.
  430. if (body_expected and not body_present) or (
  431. body_present and not body_expected
  432. ):
  433. raise ValueError(
  434. "Body must %sbe None for method %s (unless "
  435. "allow_nonstandard_methods is true)"
  436. % ("not " if body_expected else "", request.method)
  437. )
  438. if body_expected or body_present:
  439. if request.method == "GET":
  440. # Even with `allow_nonstandard_methods` we disallow
  441. # GET with a body (because libcurl doesn't allow it
  442. # unless we use CUSTOMREQUEST). While the spec doesn't
  443. # forbid clients from sending a body, it arguably
  444. # disallows the server from doing anything with them.
  445. raise ValueError("Body must be None for GET request")
  446. request_buffer = BytesIO(utf8(request.body or ""))
  447. def ioctl(cmd: int) -> None:
  448. if cmd == curl.IOCMD_RESTARTREAD: # type: ignore
  449. request_buffer.seek(0)
  450. curl.setopt(pycurl.READFUNCTION, request_buffer.read)
  451. curl.setopt(pycurl.IOCTLFUNCTION, ioctl)
  452. if request.method == "POST":
  453. curl.setopt(pycurl.POSTFIELDSIZE, len(request.body or ""))
  454. else:
  455. curl.setopt(pycurl.UPLOAD, True)
  456. curl.setopt(pycurl.INFILESIZE, len(request.body or ""))
  457. if request.auth_username is not None:
  458. assert request.auth_password is not None
  459. if request.auth_mode is None or request.auth_mode == "basic":
  460. curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
  461. elif request.auth_mode == "digest":
  462. curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_DIGEST)
  463. else:
  464. raise ValueError("Unsupported auth_mode %s" % request.auth_mode)
  465. userpwd = httputil.encode_username_password(
  466. request.auth_username, request.auth_password
  467. )
  468. curl.setopt(pycurl.USERPWD, userpwd)
  469. curl_log.debug(
  470. "%s %s (username: %r)",
  471. request.method,
  472. request.url,
  473. request.auth_username,
  474. )
  475. else:
  476. curl.unsetopt(pycurl.USERPWD)
  477. curl_log.debug("%s %s", request.method, request.url)
  478. if request.client_cert is not None:
  479. curl.setopt(pycurl.SSLCERT, request.client_cert)
  480. if request.client_key is not None:
  481. curl.setopt(pycurl.SSLKEY, request.client_key)
  482. if request.ssl_options is not None:
  483. raise ValueError("ssl_options not supported in curl_httpclient")
  484. if threading.active_count() > 1:
  485. # libcurl/pycurl is not thread-safe by default. When multiple threads
  486. # are used, signals should be disabled. This has the side effect
  487. # of disabling DNS timeouts in some environments (when libcurl is
  488. # not linked against ares), so we don't do it when there is only one
  489. # thread. Applications that use many short-lived threads may need
  490. # to set NOSIGNAL manually in a prepare_curl_callback since
  491. # there may not be any other threads running at the time we call
  492. # threading.activeCount.
  493. curl.setopt(pycurl.NOSIGNAL, 1)
  494. if request.prepare_curl_callback is not None:
  495. request.prepare_curl_callback(curl)
  496. def _curl_header_callback(
  497. self,
  498. headers: httputil.HTTPHeaders,
  499. header_callback: Callable[[str], None],
  500. header_line_bytes: bytes,
  501. ) -> None:
  502. header_line = native_str(header_line_bytes.decode("latin1"))
  503. if header_callback is not None:
  504. self.io_loop.add_callback(header_callback, header_line)
  505. # header_line as returned by curl includes the end-of-line characters.
  506. # whitespace at the start should be preserved to allow multi-line headers
  507. header_line = header_line.rstrip()
  508. if header_line.startswith("HTTP/"):
  509. headers.clear()
  510. try:
  511. (_version, _code, reason) = httputil.parse_response_start_line(
  512. header_line
  513. )
  514. header_line = "X-Http-Reason: %s" % reason
  515. except httputil.HTTPInputError:
  516. return
  517. if not header_line:
  518. return
  519. headers.parse_line(header_line)
  520. def _curl_debug(self, debug_type: int, debug_msg: str) -> None:
  521. debug_types = ("I", "<", ">", "<", ">")
  522. if debug_type == 0:
  523. debug_msg = native_str(debug_msg)
  524. curl_log.debug("%s", debug_msg.strip())
  525. elif debug_type in (1, 2):
  526. debug_msg = native_str(debug_msg)
  527. for line in debug_msg.splitlines():
  528. curl_log.debug("%s %s", debug_types[debug_type], line)
  529. elif debug_type == 4:
  530. curl_log.debug("%s %r", debug_types[debug_type], debug_msg)
  531. class CurlError(HTTPError):
  532. def __init__(self, errno: int, message: str) -> None:
  533. HTTPError.__init__(self, 599, message)
  534. self.errno = errno
  535. if __name__ == "__main__":
  536. AsyncHTTPClient.configure(CurlAsyncHTTPClient)
  537. main()