You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1511 lines
54 KiB

  1. # Copyright 2014-2021 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import abc
  15. import cgi
  16. import codecs
  17. import logging
  18. import random
  19. import sys
  20. import urllib.parse
  21. from http import HTTPStatus
  22. from io import BytesIO, StringIO
  23. from typing import (
  24. TYPE_CHECKING,
  25. Any,
  26. BinaryIO,
  27. Callable,
  28. Dict,
  29. Generic,
  30. List,
  31. Optional,
  32. TextIO,
  33. Tuple,
  34. TypeVar,
  35. Union,
  36. cast,
  37. overload,
  38. )
  39. import attr
  40. import treq
  41. from canonicaljson import encode_canonical_json
  42. from prometheus_client import Counter
  43. from signedjson.sign import sign_json
  44. from typing_extensions import Literal
  45. from twisted.internet import defer
  46. from twisted.internet.error import DNSLookupError
  47. from twisted.internet.interfaces import IReactorTime
  48. from twisted.internet.task import Cooperator
  49. from twisted.web.client import ResponseFailed
  50. from twisted.web.http_headers import Headers
  51. from twisted.web.iweb import IAgent, IBodyProducer, IResponse
  52. import synapse.metrics
  53. import synapse.util.retryutils
  54. from synapse.api.errors import (
  55. Codes,
  56. FederationDeniedError,
  57. HttpResponseException,
  58. RequestSendFailed,
  59. SynapseError,
  60. )
  61. from synapse.crypto.context_factory import FederationPolicyForHTTPS
  62. from synapse.http import QuieterFileBodyProducer
  63. from synapse.http.client import (
  64. BlocklistingAgentWrapper,
  65. BodyExceededMaxSize,
  66. ByteWriteable,
  67. _make_scheduler,
  68. encode_query_args,
  69. read_body_with_max_size,
  70. )
  71. from synapse.http.connectproxyclient import BearerProxyCredentials
  72. from synapse.http.federation.matrix_federation_agent import MatrixFederationAgent
  73. from synapse.http.proxyagent import ProxyAgent
  74. from synapse.http.types import QueryParams
  75. from synapse.logging import opentracing
  76. from synapse.logging.context import make_deferred_yieldable, run_in_background
  77. from synapse.logging.opentracing import set_tag, start_active_span, tags
  78. from synapse.types import JsonDict
  79. from synapse.util import json_decoder
  80. from synapse.util.async_helpers import AwakenableSleeper, timeout_deferred
  81. from synapse.util.metrics import Measure
  82. from synapse.util.stringutils import parse_and_validate_server_name
  83. if TYPE_CHECKING:
  84. from synapse.server import HomeServer
  85. logger = logging.getLogger(__name__)
  86. outgoing_requests_counter = Counter(
  87. "synapse_http_matrixfederationclient_requests", "", ["method"]
  88. )
  89. incoming_responses_counter = Counter(
  90. "synapse_http_matrixfederationclient_responses", "", ["method", "code"]
  91. )
  92. MAXINT = sys.maxsize
  93. _next_id = 1
  94. T = TypeVar("T")
  95. class ByteParser(ByteWriteable, Generic[T], abc.ABC):
  96. """A `ByteWriteable` that has an additional `finish` function that returns
  97. the parsed data.
  98. """
  99. CONTENT_TYPE: str = abc.abstractproperty() # type: ignore
  100. """The expected content type of the response, e.g. `application/json`. If
  101. the content type doesn't match we fail the request.
  102. """
  103. # a federation response can be rather large (eg a big state_ids is 50M or so), so we
  104. # need a generous limit here.
  105. MAX_RESPONSE_SIZE: int = 100 * 1024 * 1024
  106. """The largest response this parser will accept."""
  107. @abc.abstractmethod
  108. def finish(self) -> T:
  109. """Called when response has finished streaming and the parser should
  110. return the final result (or error).
  111. """
  112. @attr.s(slots=True, frozen=True, auto_attribs=True)
  113. class MatrixFederationRequest:
  114. method: str
  115. """HTTP method
  116. """
  117. path: str
  118. """HTTP path
  119. """
  120. destination: str
  121. """The remote server to send the HTTP request to.
  122. """
  123. json: Optional[JsonDict] = None
  124. """JSON to send in the body.
  125. """
  126. json_callback: Optional[Callable[[], JsonDict]] = None
  127. """A callback to generate the JSON.
  128. """
  129. query: Optional[QueryParams] = None
  130. """Query arguments.
  131. """
  132. txn_id: Optional[str] = None
  133. """Unique ID for this request (for logging)
  134. """
  135. uri: bytes = attr.ib(init=False)
  136. """The URI of this request
  137. """
  138. def __attrs_post_init__(self) -> None:
  139. global _next_id
  140. txn_id = "%s-O-%s" % (self.method, _next_id)
  141. _next_id = (_next_id + 1) % (MAXINT - 1)
  142. object.__setattr__(self, "txn_id", txn_id)
  143. destination_bytes = self.destination.encode("ascii")
  144. path_bytes = self.path.encode("ascii")
  145. query_bytes = encode_query_args(self.query)
  146. # The object is frozen so we can pre-compute this.
  147. uri = urllib.parse.urlunparse(
  148. (
  149. b"matrix-federation",
  150. destination_bytes,
  151. path_bytes,
  152. None,
  153. query_bytes,
  154. b"",
  155. )
  156. )
  157. object.__setattr__(self, "uri", uri)
  158. def get_json(self) -> Optional[JsonDict]:
  159. if self.json_callback:
  160. return self.json_callback()
  161. return self.json
  162. class _BaseJsonParser(ByteParser[T]):
  163. """A parser that buffers the response and tries to parse it as JSON."""
  164. CONTENT_TYPE = "application/json"
  165. def __init__(
  166. self, validator: Optional[Callable[[Optional[object]], bool]] = None
  167. ) -> None:
  168. """
  169. Args:
  170. validator: A callable which takes the parsed JSON value and returns
  171. true if the value is valid.
  172. """
  173. self._buffer = StringIO()
  174. self._binary_wrapper = BinaryIOWrapper(self._buffer)
  175. self._validator = validator
  176. def write(self, data: bytes) -> int:
  177. return self._binary_wrapper.write(data)
  178. def finish(self) -> T:
  179. result = json_decoder.decode(self._buffer.getvalue())
  180. if self._validator is not None and not self._validator(result):
  181. raise ValueError(
  182. f"Received incorrect JSON value: {result.__class__.__name__}"
  183. )
  184. return result
  185. class JsonParser(_BaseJsonParser[JsonDict]):
  186. """A parser that buffers the response and tries to parse it as a JSON object."""
  187. def __init__(self) -> None:
  188. super().__init__(self._validate)
  189. @staticmethod
  190. def _validate(v: Any) -> bool:
  191. return isinstance(v, dict)
  192. class LegacyJsonSendParser(_BaseJsonParser[Tuple[int, JsonDict]]):
  193. """Ensure the legacy responses of /send_join & /send_leave are correct."""
  194. def __init__(self) -> None:
  195. super().__init__(self._validate)
  196. @staticmethod
  197. def _validate(v: Any) -> bool:
  198. # Match [integer, JSON dict]
  199. return (
  200. isinstance(v, list)
  201. and len(v) == 2
  202. and type(v[0]) == int # noqa: E721
  203. and isinstance(v[1], dict)
  204. )
  205. async def _handle_response(
  206. reactor: IReactorTime,
  207. timeout_sec: float,
  208. request: MatrixFederationRequest,
  209. response: IResponse,
  210. start_ms: int,
  211. parser: ByteParser[T],
  212. ) -> T:
  213. """
  214. Reads the body of a response with a timeout and sends it to a parser
  215. Args:
  216. reactor: twisted reactor, for the timeout
  217. timeout_sec: number of seconds to wait for response to complete
  218. request: the request that triggered the response
  219. response: response to the request
  220. start_ms: Timestamp when request was made
  221. parser: The parser for the response
  222. Returns:
  223. The parsed response
  224. """
  225. max_response_size = parser.MAX_RESPONSE_SIZE
  226. finished = False
  227. try:
  228. check_content_type_is(response.headers, parser.CONTENT_TYPE)
  229. d = read_body_with_max_size(response, parser, max_response_size)
  230. d = timeout_deferred(d, timeout=timeout_sec, reactor=reactor)
  231. length = await make_deferred_yieldable(d)
  232. finished = True
  233. value = parser.finish()
  234. except BodyExceededMaxSize as e:
  235. # The response was too big.
  236. logger.warning(
  237. "{%s} [%s] JSON response exceeded max size %i - %s %s",
  238. request.txn_id,
  239. request.destination,
  240. max_response_size,
  241. request.method,
  242. request.uri.decode("ascii"),
  243. )
  244. raise RequestSendFailed(e, can_retry=False) from e
  245. except ValueError as e:
  246. # The content was invalid.
  247. logger.warning(
  248. "{%s} [%s] Failed to parse response - %s %s",
  249. request.txn_id,
  250. request.destination,
  251. request.method,
  252. request.uri.decode("ascii"),
  253. )
  254. raise RequestSendFailed(e, can_retry=False) from e
  255. except defer.TimeoutError as e:
  256. logger.warning(
  257. "{%s} [%s] Timed out reading response - %s %s",
  258. request.txn_id,
  259. request.destination,
  260. request.method,
  261. request.uri.decode("ascii"),
  262. )
  263. raise RequestSendFailed(e, can_retry=True) from e
  264. except ResponseFailed as e:
  265. logger.warning(
  266. "{%s} [%s] Failed to read response - %s %s",
  267. request.txn_id,
  268. request.destination,
  269. request.method,
  270. request.uri.decode("ascii"),
  271. )
  272. raise RequestSendFailed(e, can_retry=True) from e
  273. except Exception as e:
  274. logger.warning(
  275. "{%s} [%s] Error reading response %s %s: %s",
  276. request.txn_id,
  277. request.destination,
  278. request.method,
  279. request.uri.decode("ascii"),
  280. e,
  281. )
  282. raise
  283. finally:
  284. if not finished:
  285. # There was an exception and we didn't `finish()` the parse.
  286. # Let the parser know that it can free up any resources.
  287. try:
  288. parser.finish()
  289. except Exception:
  290. # Ignore any additional exceptions.
  291. pass
  292. time_taken_secs = reactor.seconds() - start_ms / 1000
  293. logger.info(
  294. "{%s} [%s] Completed request: %d %s in %.2f secs, got %d bytes - %s %s",
  295. request.txn_id,
  296. request.destination,
  297. response.code,
  298. response.phrase.decode("ascii", errors="replace"),
  299. time_taken_secs,
  300. length,
  301. request.method,
  302. request.uri.decode("ascii"),
  303. )
  304. return value
  305. class BinaryIOWrapper:
  306. """A wrapper for a TextIO which converts from bytes on the fly."""
  307. def __init__(self, file: TextIO, encoding: str = "utf-8", errors: str = "strict"):
  308. self.decoder = codecs.getincrementaldecoder(encoding)(errors)
  309. self.file = file
  310. def write(self, b: Union[bytes, bytearray]) -> int:
  311. self.file.write(self.decoder.decode(b))
  312. return len(b)
  313. class MatrixFederationHttpClient:
  314. """HTTP client used to talk to other homeservers over the federation
  315. protocol. Send client certificates and signs requests.
  316. Attributes:
  317. agent (twisted.web.client.Agent): The twisted Agent used to send the
  318. requests.
  319. """
  320. def __init__(
  321. self,
  322. hs: "HomeServer",
  323. tls_client_options_factory: Optional[FederationPolicyForHTTPS],
  324. ):
  325. self.hs = hs
  326. self.signing_key = hs.signing_key
  327. self.server_name = hs.hostname
  328. self.reactor = hs.get_reactor()
  329. user_agent = hs.version_string
  330. if hs.config.server.user_agent_suffix:
  331. user_agent = "%s %s" % (user_agent, hs.config.server.user_agent_suffix)
  332. outbound_federation_restricted_to = (
  333. hs.config.worker.outbound_federation_restricted_to
  334. )
  335. if hs.get_instance_name() in outbound_federation_restricted_to:
  336. # Talk to federation directly
  337. federation_agent: IAgent = MatrixFederationAgent(
  338. self.reactor,
  339. tls_client_options_factory,
  340. user_agent.encode("ascii"),
  341. hs.config.server.federation_ip_range_allowlist,
  342. hs.config.server.federation_ip_range_blocklist,
  343. )
  344. else:
  345. proxy_authorization_secret = hs.config.worker.worker_replication_secret
  346. assert (
  347. proxy_authorization_secret is not None
  348. ), "`worker_replication_secret` must be set when using `outbound_federation_restricted_to` (used to authenticate requests across workers)"
  349. federation_proxy_credentials = BearerProxyCredentials(
  350. proxy_authorization_secret.encode("ascii")
  351. )
  352. # We need to talk to federation via the proxy via one of the configured
  353. # locations
  354. federation_proxy_locations = outbound_federation_restricted_to.locations
  355. federation_agent = ProxyAgent(
  356. self.reactor,
  357. self.reactor,
  358. tls_client_options_factory,
  359. federation_proxy_locations=federation_proxy_locations,
  360. federation_proxy_credentials=federation_proxy_credentials,
  361. )
  362. # Use a BlocklistingAgentWrapper to prevent circumventing the IP
  363. # blocking via IP literals in server names
  364. self.agent: IAgent = BlocklistingAgentWrapper(
  365. federation_agent,
  366. ip_blocklist=hs.config.server.federation_ip_range_blocklist,
  367. )
  368. self.clock = hs.get_clock()
  369. self._store = hs.get_datastores().main
  370. self.version_string_bytes = hs.version_string.encode("ascii")
  371. self.default_timeout_seconds = hs.config.federation.client_timeout_ms / 1000
  372. self.max_long_retry_delay_seconds = (
  373. hs.config.federation.max_long_retry_delay_ms / 1000
  374. )
  375. self.max_short_retry_delay_seconds = (
  376. hs.config.federation.max_short_retry_delay_ms / 1000
  377. )
  378. self.max_long_retries = hs.config.federation.max_long_retries
  379. self.max_short_retries = hs.config.federation.max_short_retries
  380. self._cooperator = Cooperator(scheduler=_make_scheduler(self.reactor))
  381. self._sleeper = AwakenableSleeper(self.reactor)
  382. def wake_destination(self, destination: str) -> None:
  383. """Called when the remote server may have come back online."""
  384. self._sleeper.wake(destination)
  385. async def _send_request_with_optional_trailing_slash(
  386. self,
  387. request: MatrixFederationRequest,
  388. try_trailing_slash_on_400: bool = False,
  389. **send_request_args: Any,
  390. ) -> IResponse:
  391. """Wrapper for _send_request which can optionally retry the request
  392. upon receiving a combination of a 400 HTTP response code and a
  393. 'M_UNRECOGNIZED' errcode. This is a workaround for Synapse <= v0.99.3
  394. due to https://github.com/matrix-org/synapse/issues/3622.
  395. Args:
  396. request: details of request to be sent
  397. try_trailing_slash_on_400: Whether on receiving a 400
  398. 'M_UNRECOGNIZED' from the server to retry the request with a
  399. trailing slash appended to the request path.
  400. send_request_args: A dictionary of arguments to pass to `_send_request()`.
  401. Raises:
  402. HttpResponseException: If we get an HTTP response code >= 300
  403. (except 429).
  404. Returns:
  405. Parsed JSON response body.
  406. """
  407. try:
  408. response = await self._send_request(request, **send_request_args)
  409. except HttpResponseException as e:
  410. # Received an HTTP error > 300. Check if it meets the requirements
  411. # to retry with a trailing slash
  412. if not try_trailing_slash_on_400:
  413. raise
  414. if e.code != 400 or e.to_synapse_error().errcode != "M_UNRECOGNIZED":
  415. raise
  416. # Retry with a trailing slash if we received a 400 with
  417. # 'M_UNRECOGNIZED' which some endpoints can return when omitting a
  418. # trailing slash on Synapse <= v0.99.3.
  419. logger.info("Retrying request with trailing slash")
  420. # Request is frozen so we create a new instance
  421. request = attr.evolve(request, path=request.path + "/")
  422. response = await self._send_request(request, **send_request_args)
  423. return response
  424. async def _send_request(
  425. self,
  426. request: MatrixFederationRequest,
  427. retry_on_dns_fail: bool = True,
  428. timeout: Optional[int] = None,
  429. long_retries: bool = False,
  430. ignore_backoff: bool = False,
  431. backoff_on_404: bool = False,
  432. backoff_on_all_error_codes: bool = False,
  433. ) -> IResponse:
  434. """
  435. Sends a request to the given server.
  436. Args:
  437. request: details of request to be sent
  438. retry_on_dns_fail: true if the request should be retried on DNS failures
  439. timeout: number of milliseconds to wait for the response headers
  440. (including connecting to the server), *for each attempt*.
  441. 60s by default.
  442. long_retries: whether to use the long retry algorithm.
  443. The regular retry algorithm makes 4 attempts, with intervals
  444. [0.5s, 1s, 2s].
  445. The long retry algorithm makes 11 attempts, with intervals
  446. [4s, 16s, 60s, 60s, ...]
  447. Both algorithms add -20%/+40% jitter to the retry intervals.
  448. Note that the above intervals are *in addition* to the time spent
  449. waiting for the request to complete (up to `timeout` ms).
  450. NB: the long retry algorithm takes over 20 minutes to complete, with a
  451. default timeout of 60s! It's best not to use the `long_retries` option
  452. for something that is blocking a client so we don't make them wait for
  453. aaaaages, whereas some things like sending transactions (server to
  454. server) we can be a lot more lenient but its very fuzzy / hand-wavey.
  455. In the future, we could be more intelligent about doing this sort of
  456. thing by looking at things with the bigger picture in mind,
  457. https://github.com/matrix-org/synapse/issues/8917
  458. ignore_backoff: true to ignore the historical backoff data
  459. and try the request anyway.
  460. backoff_on_404: Back off if we get a 404
  461. backoff_on_all_error_codes: Back off if we get any error response
  462. Returns:
  463. Resolves with the HTTP response object on success.
  464. Raises:
  465. HttpResponseException: If we get an HTTP response code >= 300
  466. (except 429).
  467. NotRetryingDestination: If we are not yet ready to retry this
  468. server.
  469. FederationDeniedError: If this destination is not on our
  470. federation whitelist
  471. RequestSendFailed: If there were problems connecting to the
  472. remote, due to e.g. DNS failures, connection timeouts etc.
  473. """
  474. # Validate server name and log if it is an invalid destination, this is
  475. # partially to help track down code paths where we haven't validated before here
  476. try:
  477. parse_and_validate_server_name(request.destination)
  478. except ValueError:
  479. logger.exception(f"Invalid destination: {request.destination}.")
  480. raise FederationDeniedError(request.destination)
  481. if timeout is not None:
  482. _sec_timeout = timeout / 1000
  483. else:
  484. _sec_timeout = self.default_timeout_seconds
  485. if (
  486. self.hs.config.federation.federation_domain_whitelist is not None
  487. and request.destination
  488. not in self.hs.config.federation.federation_domain_whitelist
  489. ):
  490. raise FederationDeniedError(request.destination)
  491. limiter = await synapse.util.retryutils.get_retry_limiter(
  492. request.destination,
  493. self.clock,
  494. self._store,
  495. backoff_on_404=backoff_on_404,
  496. ignore_backoff=ignore_backoff,
  497. notifier=self.hs.get_notifier(),
  498. replication_client=self.hs.get_replication_command_handler(),
  499. backoff_on_all_error_codes=backoff_on_all_error_codes,
  500. )
  501. method_bytes = request.method.encode("ascii")
  502. destination_bytes = request.destination.encode("ascii")
  503. path_bytes = request.path.encode("ascii")
  504. query_bytes = encode_query_args(request.query)
  505. scope = start_active_span(
  506. "outgoing-federation-request",
  507. tags={
  508. tags.SPAN_KIND: tags.SPAN_KIND_RPC_CLIENT,
  509. tags.PEER_ADDRESS: request.destination,
  510. tags.HTTP_METHOD: request.method,
  511. tags.HTTP_URL: request.path,
  512. },
  513. finish_on_close=True,
  514. )
  515. # Inject the span into the headers
  516. headers_dict: Dict[bytes, List[bytes]] = {}
  517. opentracing.inject_header_dict(headers_dict, request.destination)
  518. headers_dict[b"User-Agent"] = [self.version_string_bytes]
  519. with limiter, scope:
  520. # XXX: Would be much nicer to retry only at the transaction-layer
  521. # (once we have reliable transactions in place)
  522. if long_retries:
  523. retries_left = self.max_long_retries
  524. else:
  525. retries_left = self.max_short_retries
  526. url_bytes = request.uri
  527. url_str = url_bytes.decode("ascii")
  528. url_to_sign_bytes = urllib.parse.urlunparse(
  529. (b"", b"", path_bytes, None, query_bytes, b"")
  530. )
  531. while True:
  532. try:
  533. json = request.get_json()
  534. if json:
  535. headers_dict[b"Content-Type"] = [b"application/json"]
  536. auth_headers = self.build_auth_headers(
  537. destination_bytes, method_bytes, url_to_sign_bytes, json
  538. )
  539. data = encode_canonical_json(json)
  540. producer: Optional[IBodyProducer] = QuieterFileBodyProducer(
  541. BytesIO(data), cooperator=self._cooperator
  542. )
  543. else:
  544. producer = None
  545. auth_headers = self.build_auth_headers(
  546. destination_bytes, method_bytes, url_to_sign_bytes
  547. )
  548. headers_dict[b"Authorization"] = auth_headers
  549. logger.debug(
  550. "{%s} [%s] Sending request: %s %s; timeout %fs",
  551. request.txn_id,
  552. request.destination,
  553. request.method,
  554. url_str,
  555. _sec_timeout,
  556. )
  557. outgoing_requests_counter.labels(request.method).inc()
  558. try:
  559. with Measure(self.clock, "outbound_request"):
  560. # we don't want all the fancy cookie and redirect handling
  561. # that treq.request gives: just use the raw Agent.
  562. # To preserve the logging context, the timeout is treated
  563. # in a similar way to `defer.gatherResults`:
  564. # * Each logging context-preserving fork is wrapped in
  565. # `run_in_background`. In this case there is only one,
  566. # since the timeout fork is not logging-context aware.
  567. # * The `Deferred` that joins the forks back together is
  568. # wrapped in `make_deferred_yieldable` to restore the
  569. # logging context regardless of the path taken.
  570. request_deferred = run_in_background(
  571. self.agent.request,
  572. method_bytes,
  573. url_bytes,
  574. headers=Headers(headers_dict),
  575. bodyProducer=producer,
  576. )
  577. request_deferred = timeout_deferred(
  578. request_deferred,
  579. timeout=_sec_timeout,
  580. reactor=self.reactor,
  581. )
  582. response = await make_deferred_yieldable(request_deferred)
  583. except DNSLookupError as e:
  584. raise RequestSendFailed(e, can_retry=retry_on_dns_fail) from e
  585. except Exception as e:
  586. raise RequestSendFailed(e, can_retry=True) from e
  587. incoming_responses_counter.labels(
  588. request.method, response.code
  589. ).inc()
  590. set_tag(tags.HTTP_STATUS_CODE, response.code)
  591. response_phrase = response.phrase.decode("ascii", errors="replace")
  592. if 200 <= response.code < 300:
  593. logger.debug(
  594. "{%s} [%s] Got response headers: %d %s",
  595. request.txn_id,
  596. request.destination,
  597. response.code,
  598. response_phrase,
  599. )
  600. else:
  601. logger.info(
  602. "{%s} [%s] Got response headers: %d %s",
  603. request.txn_id,
  604. request.destination,
  605. response.code,
  606. response_phrase,
  607. )
  608. # :'(
  609. # Update transactions table?
  610. d = treq.content(response)
  611. d = timeout_deferred(
  612. d, timeout=_sec_timeout, reactor=self.reactor
  613. )
  614. try:
  615. body = await make_deferred_yieldable(d)
  616. except Exception as e:
  617. # Eh, we're already going to raise an exception so lets
  618. # ignore if this fails.
  619. logger.warning(
  620. "{%s} [%s] Failed to get error response: %s %s: %s",
  621. request.txn_id,
  622. request.destination,
  623. request.method,
  624. url_str,
  625. _flatten_response_never_received(e),
  626. )
  627. body = None
  628. exc = HttpResponseException(
  629. response.code, response_phrase, body
  630. )
  631. # Retry if the error is a 5xx or a 429 (Too Many
  632. # Requests), otherwise just raise a standard
  633. # `HttpResponseException`
  634. if 500 <= response.code < 600 or response.code == 429:
  635. raise RequestSendFailed(exc, can_retry=True) from exc
  636. else:
  637. raise exc
  638. break
  639. except RequestSendFailed as e:
  640. logger.info(
  641. "{%s} [%s] Request failed: %s %s: %s",
  642. request.txn_id,
  643. request.destination,
  644. request.method,
  645. url_str,
  646. _flatten_response_never_received(e.inner_exception),
  647. )
  648. if not e.can_retry:
  649. raise
  650. if retries_left and not timeout:
  651. if long_retries:
  652. delay_seconds = 4 ** (
  653. self.max_long_retries + 1 - retries_left
  654. )
  655. delay_seconds = min(
  656. delay_seconds, self.max_long_retry_delay_seconds
  657. )
  658. delay_seconds *= random.uniform(0.8, 1.4)
  659. else:
  660. delay_seconds = 0.5 * 2 ** (
  661. self.max_short_retries - retries_left
  662. )
  663. delay_seconds = min(
  664. delay_seconds, self.max_short_retry_delay_seconds
  665. )
  666. delay_seconds *= random.uniform(0.8, 1.4)
  667. logger.debug(
  668. "{%s} [%s] Waiting %ss before re-sending...",
  669. request.txn_id,
  670. request.destination,
  671. delay_seconds,
  672. )
  673. # Sleep for the calculated delay, or wake up immediately
  674. # if we get notified that the server is back up.
  675. await self._sleeper.sleep(
  676. request.destination, delay_seconds * 1000
  677. )
  678. retries_left -= 1
  679. else:
  680. raise
  681. except Exception as e:
  682. logger.warning(
  683. "{%s} [%s] Request failed: %s %s: %s",
  684. request.txn_id,
  685. request.destination,
  686. request.method,
  687. url_str,
  688. _flatten_response_never_received(e),
  689. )
  690. raise
  691. return response
  692. def build_auth_headers(
  693. self,
  694. destination: Optional[bytes],
  695. method: bytes,
  696. url_bytes: bytes,
  697. content: Optional[JsonDict] = None,
  698. destination_is: Optional[bytes] = None,
  699. ) -> List[bytes]:
  700. """
  701. Builds the Authorization headers for a federation request
  702. Args:
  703. destination: The destination homeserver of the request.
  704. May be None if the destination is an identity server, in which case
  705. destination_is must be non-None.
  706. method: The HTTP method of the request
  707. url_bytes: The URI path of the request
  708. content: The body of the request
  709. destination_is: As 'destination', but if the destination is an
  710. identity server
  711. Returns:
  712. A list of headers to be added as "Authorization:" headers
  713. """
  714. if not destination and not destination_is:
  715. raise ValueError(
  716. "At least one of the arguments destination and destination_is "
  717. "must be a nonempty bytestring."
  718. )
  719. request: JsonDict = {
  720. "method": method.decode("ascii"),
  721. "uri": url_bytes.decode("ascii"),
  722. "origin": self.server_name,
  723. }
  724. if destination is not None:
  725. request["destination"] = destination.decode("ascii")
  726. if destination_is is not None:
  727. request["destination_is"] = destination_is.decode("ascii")
  728. if content is not None:
  729. request["content"] = content
  730. request = sign_json(request, self.server_name, self.signing_key)
  731. auth_headers = []
  732. for key, sig in request["signatures"][self.server_name].items():
  733. auth_headers.append(
  734. (
  735. 'X-Matrix origin="%s",key="%s",sig="%s",destination="%s"'
  736. % (
  737. self.server_name,
  738. key,
  739. sig,
  740. request.get("destination") or request["destination_is"],
  741. )
  742. ).encode("ascii")
  743. )
  744. return auth_headers
  745. @overload
  746. async def put_json(
  747. self,
  748. destination: str,
  749. path: str,
  750. args: Optional[QueryParams] = None,
  751. data: Optional[JsonDict] = None,
  752. json_data_callback: Optional[Callable[[], JsonDict]] = None,
  753. long_retries: bool = False,
  754. timeout: Optional[int] = None,
  755. ignore_backoff: bool = False,
  756. backoff_on_404: bool = False,
  757. try_trailing_slash_on_400: bool = False,
  758. parser: Literal[None] = None,
  759. backoff_on_all_error_codes: bool = False,
  760. ) -> JsonDict:
  761. ...
  762. @overload
  763. async def put_json(
  764. self,
  765. destination: str,
  766. path: str,
  767. args: Optional[QueryParams] = None,
  768. data: Optional[JsonDict] = None,
  769. json_data_callback: Optional[Callable[[], JsonDict]] = None,
  770. long_retries: bool = False,
  771. timeout: Optional[int] = None,
  772. ignore_backoff: bool = False,
  773. backoff_on_404: bool = False,
  774. try_trailing_slash_on_400: bool = False,
  775. parser: Optional[ByteParser[T]] = None,
  776. backoff_on_all_error_codes: bool = False,
  777. ) -> T:
  778. ...
  779. async def put_json(
  780. self,
  781. destination: str,
  782. path: str,
  783. args: Optional[QueryParams] = None,
  784. data: Optional[JsonDict] = None,
  785. json_data_callback: Optional[Callable[[], JsonDict]] = None,
  786. long_retries: bool = False,
  787. timeout: Optional[int] = None,
  788. ignore_backoff: bool = False,
  789. backoff_on_404: bool = False,
  790. try_trailing_slash_on_400: bool = False,
  791. parser: Optional[ByteParser[T]] = None,
  792. backoff_on_all_error_codes: bool = False,
  793. ) -> Union[JsonDict, T]:
  794. """Sends the specified json data using PUT
  795. Args:
  796. destination: The remote server to send the HTTP request to.
  797. path: The HTTP path.
  798. args: query params
  799. data: A dict containing the data that will be used as
  800. the request body. This will be encoded as JSON.
  801. json_data_callback: A callable returning the dict to
  802. use as the request body.
  803. long_retries: whether to use the long retry algorithm. See
  804. docs on _send_request for details.
  805. timeout: number of milliseconds to wait for the response.
  806. self._default_timeout (60s) by default.
  807. Note that we may make several attempts to send the request; this
  808. timeout applies to the time spent waiting for response headers for
  809. *each* attempt (including connection time) as well as the time spent
  810. reading the response body after a 200 response.
  811. ignore_backoff: true to ignore the historical backoff data
  812. and try the request anyway.
  813. backoff_on_404: True if we should count a 404 response as
  814. a failure of the server (and should therefore back off future
  815. requests).
  816. try_trailing_slash_on_400: True if on a 400 M_UNRECOGNIZED
  817. response we should try appending a trailing slash to the end
  818. of the request. Workaround for https://github.com/matrix-org/synapse/issues/3622
  819. in Synapse <= v0.99.3. This will be attempted before backing off if
  820. backing off has been enabled.
  821. parser: The parser to use to decode the response. Defaults to
  822. parsing as JSON.
  823. backoff_on_all_error_codes: Back off if we get any error response
  824. Returns:
  825. Succeeds when we get a 2xx HTTP response. The
  826. result will be the decoded JSON body.
  827. Raises:
  828. HttpResponseException: If we get an HTTP response code >= 300
  829. (except 429).
  830. NotRetryingDestination: If we are not yet ready to retry this
  831. server.
  832. FederationDeniedError: If this destination is not on our
  833. federation whitelist
  834. RequestSendFailed: If there were problems connecting to the
  835. remote, due to e.g. DNS failures, connection timeouts etc.
  836. """
  837. request = MatrixFederationRequest(
  838. method="PUT",
  839. destination=destination,
  840. path=path,
  841. query=args,
  842. json_callback=json_data_callback,
  843. json=data,
  844. )
  845. start_ms = self.clock.time_msec()
  846. response = await self._send_request_with_optional_trailing_slash(
  847. request,
  848. try_trailing_slash_on_400,
  849. backoff_on_404=backoff_on_404,
  850. ignore_backoff=ignore_backoff,
  851. long_retries=long_retries,
  852. timeout=timeout,
  853. backoff_on_all_error_codes=backoff_on_all_error_codes,
  854. )
  855. if timeout is not None:
  856. _sec_timeout = timeout / 1000
  857. else:
  858. _sec_timeout = self.default_timeout_seconds
  859. if parser is None:
  860. parser = cast(ByteParser[T], JsonParser())
  861. body = await _handle_response(
  862. self.reactor,
  863. _sec_timeout,
  864. request,
  865. response,
  866. start_ms,
  867. parser=parser,
  868. )
  869. return body
  870. async def post_json(
  871. self,
  872. destination: str,
  873. path: str,
  874. data: Optional[JsonDict] = None,
  875. long_retries: bool = False,
  876. timeout: Optional[int] = None,
  877. ignore_backoff: bool = False,
  878. args: Optional[QueryParams] = None,
  879. ) -> JsonDict:
  880. """Sends the specified json data using POST
  881. Args:
  882. destination: The remote server to send the HTTP request to.
  883. path: The HTTP path.
  884. data: A dict containing the data that will be used as
  885. the request body. This will be encoded as JSON.
  886. long_retries: whether to use the long retry algorithm. See
  887. docs on _send_request for details.
  888. timeout: number of milliseconds to wait for the response.
  889. self._default_timeout (60s) by default.
  890. Note that we may make several attempts to send the request; this
  891. timeout applies to the time spent waiting for response headers for
  892. *each* attempt (including connection time) as well as the time spent
  893. reading the response body after a 200 response.
  894. ignore_backoff: true to ignore the historical backoff data and
  895. try the request anyway.
  896. args: query params
  897. Returns:
  898. Succeeds when we get a 2xx HTTP response. The result will be the decoded JSON body.
  899. Raises:
  900. HttpResponseException: If we get an HTTP response code >= 300
  901. (except 429).
  902. NotRetryingDestination: If we are not yet ready to retry this
  903. server.
  904. FederationDeniedError: If this destination is not on our
  905. federation whitelist
  906. RequestSendFailed: If there were problems connecting to the
  907. remote, due to e.g. DNS failures, connection timeouts etc.
  908. """
  909. request = MatrixFederationRequest(
  910. method="POST", destination=destination, path=path, query=args, json=data
  911. )
  912. start_ms = self.clock.time_msec()
  913. response = await self._send_request(
  914. request,
  915. long_retries=long_retries,
  916. timeout=timeout,
  917. ignore_backoff=ignore_backoff,
  918. )
  919. if timeout is not None:
  920. _sec_timeout = timeout / 1000
  921. else:
  922. _sec_timeout = self.default_timeout_seconds
  923. body = await _handle_response(
  924. self.reactor, _sec_timeout, request, response, start_ms, parser=JsonParser()
  925. )
  926. return body
  927. @overload
  928. async def get_json(
  929. self,
  930. destination: str,
  931. path: str,
  932. args: Optional[QueryParams] = None,
  933. retry_on_dns_fail: bool = True,
  934. timeout: Optional[int] = None,
  935. ignore_backoff: bool = False,
  936. try_trailing_slash_on_400: bool = False,
  937. parser: Literal[None] = None,
  938. ) -> JsonDict:
  939. ...
  940. @overload
  941. async def get_json(
  942. self,
  943. destination: str,
  944. path: str,
  945. args: Optional[QueryParams] = ...,
  946. retry_on_dns_fail: bool = ...,
  947. timeout: Optional[int] = ...,
  948. ignore_backoff: bool = ...,
  949. try_trailing_slash_on_400: bool = ...,
  950. parser: ByteParser[T] = ...,
  951. ) -> T:
  952. ...
  953. async def get_json(
  954. self,
  955. destination: str,
  956. path: str,
  957. args: Optional[QueryParams] = None,
  958. retry_on_dns_fail: bool = True,
  959. timeout: Optional[int] = None,
  960. ignore_backoff: bool = False,
  961. try_trailing_slash_on_400: bool = False,
  962. parser: Optional[ByteParser[T]] = None,
  963. ) -> Union[JsonDict, T]:
  964. """GETs some json from the given host homeserver and path
  965. Args:
  966. destination: The remote server to send the HTTP request to.
  967. path: The HTTP path.
  968. args: A dictionary used to create query strings, defaults to
  969. None.
  970. retry_on_dns_fail: true if the request should be retried on DNS failures
  971. timeout: number of milliseconds to wait for the response.
  972. self._default_timeout (60s) by default.
  973. Note that we may make several attempts to send the request; this
  974. timeout applies to the time spent waiting for response headers for
  975. *each* attempt (including connection time) as well as the time spent
  976. reading the response body after a 200 response.
  977. ignore_backoff: true to ignore the historical backoff data
  978. and try the request anyway.
  979. try_trailing_slash_on_400: True if on a 400 M_UNRECOGNIZED
  980. response we should try appending a trailing slash to the end of
  981. the request. Workaround for https://github.com/matrix-org/synapse/issues/3622
  982. in Synapse <= v0.99.3.
  983. parser: The parser to use to decode the response. Defaults to
  984. parsing as JSON.
  985. Returns:
  986. Succeeds when we get a 2xx HTTP response. The
  987. result will be the decoded JSON body.
  988. Raises:
  989. HttpResponseException: If we get an HTTP response code >= 300
  990. (except 429).
  991. NotRetryingDestination: If we are not yet ready to retry this
  992. server.
  993. FederationDeniedError: If this destination is not on our
  994. federation whitelist
  995. RequestSendFailed: If there were problems connecting to the
  996. remote, due to e.g. DNS failures, connection timeouts etc.
  997. """
  998. json_dict, _ = await self.get_json_with_headers(
  999. destination=destination,
  1000. path=path,
  1001. args=args,
  1002. retry_on_dns_fail=retry_on_dns_fail,
  1003. timeout=timeout,
  1004. ignore_backoff=ignore_backoff,
  1005. try_trailing_slash_on_400=try_trailing_slash_on_400,
  1006. parser=parser,
  1007. )
  1008. return json_dict
  1009. @overload
  1010. async def get_json_with_headers(
  1011. self,
  1012. destination: str,
  1013. path: str,
  1014. args: Optional[QueryParams] = None,
  1015. retry_on_dns_fail: bool = True,
  1016. timeout: Optional[int] = None,
  1017. ignore_backoff: bool = False,
  1018. try_trailing_slash_on_400: bool = False,
  1019. parser: Literal[None] = None,
  1020. ) -> Tuple[JsonDict, Dict[bytes, List[bytes]]]:
  1021. ...
  1022. @overload
  1023. async def get_json_with_headers(
  1024. self,
  1025. destination: str,
  1026. path: str,
  1027. args: Optional[QueryParams] = ...,
  1028. retry_on_dns_fail: bool = ...,
  1029. timeout: Optional[int] = ...,
  1030. ignore_backoff: bool = ...,
  1031. try_trailing_slash_on_400: bool = ...,
  1032. parser: ByteParser[T] = ...,
  1033. ) -> Tuple[T, Dict[bytes, List[bytes]]]:
  1034. ...
  1035. async def get_json_with_headers(
  1036. self,
  1037. destination: str,
  1038. path: str,
  1039. args: Optional[QueryParams] = None,
  1040. retry_on_dns_fail: bool = True,
  1041. timeout: Optional[int] = None,
  1042. ignore_backoff: bool = False,
  1043. try_trailing_slash_on_400: bool = False,
  1044. parser: Optional[ByteParser[T]] = None,
  1045. ) -> Tuple[Union[JsonDict, T], Dict[bytes, List[bytes]]]:
  1046. """GETs some json from the given host homeserver and path
  1047. Args:
  1048. destination: The remote server to send the HTTP request to.
  1049. path: The HTTP path.
  1050. args: A dictionary used to create query strings, defaults to
  1051. None.
  1052. retry_on_dns_fail: true if the request should be retried on DNS failures
  1053. timeout: number of milliseconds to wait for the response.
  1054. self._default_timeout (60s) by default.
  1055. Note that we may make several attempts to send the request; this
  1056. timeout applies to the time spent waiting for response headers for
  1057. *each* attempt (including connection time) as well as the time spent
  1058. reading the response body after a 200 response.
  1059. ignore_backoff: true to ignore the historical backoff data
  1060. and try the request anyway.
  1061. try_trailing_slash_on_400: True if on a 400 M_UNRECOGNIZED
  1062. response we should try appending a trailing slash to the end of
  1063. the request. Workaround for https://github.com/matrix-org/synapse/issues/3622
  1064. in Synapse <= v0.99.3.
  1065. parser: The parser to use to decode the response. Defaults to
  1066. parsing as JSON.
  1067. Returns:
  1068. Succeeds when we get a 2xx HTTP response. The result will be a tuple of the
  1069. decoded JSON body and a dict of the response headers.
  1070. Raises:
  1071. HttpResponseException: If we get an HTTP response code >= 300
  1072. (except 429).
  1073. NotRetryingDestination: If we are not yet ready to retry this
  1074. server.
  1075. FederationDeniedError: If this destination is not on our
  1076. federation whitelist
  1077. RequestSendFailed: If there were problems connecting to the
  1078. remote, due to e.g. DNS failures, connection timeouts etc.
  1079. """
  1080. request = MatrixFederationRequest(
  1081. method="GET", destination=destination, path=path, query=args
  1082. )
  1083. start_ms = self.clock.time_msec()
  1084. response = await self._send_request_with_optional_trailing_slash(
  1085. request,
  1086. try_trailing_slash_on_400,
  1087. backoff_on_404=False,
  1088. ignore_backoff=ignore_backoff,
  1089. retry_on_dns_fail=retry_on_dns_fail,
  1090. timeout=timeout,
  1091. )
  1092. headers = dict(response.headers.getAllRawHeaders())
  1093. if timeout is not None:
  1094. _sec_timeout = timeout / 1000
  1095. else:
  1096. _sec_timeout = self.default_timeout_seconds
  1097. if parser is None:
  1098. parser = cast(ByteParser[T], JsonParser())
  1099. body = await _handle_response(
  1100. self.reactor,
  1101. _sec_timeout,
  1102. request,
  1103. response,
  1104. start_ms,
  1105. parser=parser,
  1106. )
  1107. return body, headers
  1108. async def delete_json(
  1109. self,
  1110. destination: str,
  1111. path: str,
  1112. long_retries: bool = False,
  1113. timeout: Optional[int] = None,
  1114. ignore_backoff: bool = False,
  1115. args: Optional[QueryParams] = None,
  1116. ) -> JsonDict:
  1117. """Send a DELETE request to the remote expecting some json response
  1118. Args:
  1119. destination: The remote server to send the HTTP request to.
  1120. path: The HTTP path.
  1121. long_retries: whether to use the long retry algorithm. See
  1122. docs on _send_request for details.
  1123. timeout: number of milliseconds to wait for the response.
  1124. self._default_timeout (60s) by default.
  1125. Note that we may make several attempts to send the request; this
  1126. timeout applies to the time spent waiting for response headers for
  1127. *each* attempt (including connection time) as well as the time spent
  1128. reading the response body after a 200 response.
  1129. ignore_backoff: true to ignore the historical backoff data and
  1130. try the request anyway.
  1131. args: query params
  1132. Returns:
  1133. Succeeds when we get a 2xx HTTP response. The
  1134. result will be the decoded JSON body.
  1135. Raises:
  1136. HttpResponseException: If we get an HTTP response code >= 300
  1137. (except 429).
  1138. NotRetryingDestination: If we are not yet ready to retry this
  1139. server.
  1140. FederationDeniedError: If this destination is not on our
  1141. federation whitelist
  1142. RequestSendFailed: If there were problems connecting to the
  1143. remote, due to e.g. DNS failures, connection timeouts etc.
  1144. """
  1145. request = MatrixFederationRequest(
  1146. method="DELETE", destination=destination, path=path, query=args
  1147. )
  1148. start_ms = self.clock.time_msec()
  1149. response = await self._send_request(
  1150. request,
  1151. long_retries=long_retries,
  1152. timeout=timeout,
  1153. ignore_backoff=ignore_backoff,
  1154. )
  1155. if timeout is not None:
  1156. _sec_timeout = timeout / 1000
  1157. else:
  1158. _sec_timeout = self.default_timeout_seconds
  1159. body = await _handle_response(
  1160. self.reactor, _sec_timeout, request, response, start_ms, parser=JsonParser()
  1161. )
  1162. return body
  1163. async def get_file(
  1164. self,
  1165. destination: str,
  1166. path: str,
  1167. output_stream: BinaryIO,
  1168. args: Optional[QueryParams] = None,
  1169. retry_on_dns_fail: bool = True,
  1170. max_size: Optional[int] = None,
  1171. ignore_backoff: bool = False,
  1172. ) -> Tuple[int, Dict[bytes, List[bytes]]]:
  1173. """GETs a file from a given homeserver
  1174. Args:
  1175. destination: The remote server to send the HTTP request to.
  1176. path: The HTTP path to GET.
  1177. output_stream: File to write the response body to.
  1178. args: Optional dictionary used to create the query string.
  1179. ignore_backoff: true to ignore the historical backoff data
  1180. and try the request anyway.
  1181. Returns:
  1182. Resolves with an (int,dict) tuple of
  1183. the file length and a dict of the response headers.
  1184. Raises:
  1185. HttpResponseException: If we get an HTTP response code >= 300
  1186. (except 429).
  1187. NotRetryingDestination: If we are not yet ready to retry this
  1188. server.
  1189. FederationDeniedError: If this destination is not on our
  1190. federation whitelist
  1191. RequestSendFailed: If there were problems connecting to the
  1192. remote, due to e.g. DNS failures, connection timeouts etc.
  1193. """
  1194. request = MatrixFederationRequest(
  1195. method="GET", destination=destination, path=path, query=args
  1196. )
  1197. response = await self._send_request(
  1198. request, retry_on_dns_fail=retry_on_dns_fail, ignore_backoff=ignore_backoff
  1199. )
  1200. headers = dict(response.headers.getAllRawHeaders())
  1201. try:
  1202. d = read_body_with_max_size(response, output_stream, max_size)
  1203. d.addTimeout(self.default_timeout_seconds, self.reactor)
  1204. length = await make_deferred_yieldable(d)
  1205. except BodyExceededMaxSize:
  1206. msg = "Requested file is too large > %r bytes" % (max_size,)
  1207. logger.warning(
  1208. "{%s} [%s] %s",
  1209. request.txn_id,
  1210. request.destination,
  1211. msg,
  1212. )
  1213. raise SynapseError(HTTPStatus.BAD_GATEWAY, msg, Codes.TOO_LARGE)
  1214. except defer.TimeoutError as e:
  1215. logger.warning(
  1216. "{%s} [%s] Timed out reading response - %s %s",
  1217. request.txn_id,
  1218. request.destination,
  1219. request.method,
  1220. request.uri.decode("ascii"),
  1221. )
  1222. raise RequestSendFailed(e, can_retry=True) from e
  1223. except ResponseFailed as e:
  1224. logger.warning(
  1225. "{%s} [%s] Failed to read response - %s %s",
  1226. request.txn_id,
  1227. request.destination,
  1228. request.method,
  1229. request.uri.decode("ascii"),
  1230. )
  1231. raise RequestSendFailed(e, can_retry=True) from e
  1232. except Exception as e:
  1233. logger.warning(
  1234. "{%s} [%s] Error reading response: %s",
  1235. request.txn_id,
  1236. request.destination,
  1237. e,
  1238. )
  1239. raise
  1240. logger.info(
  1241. "{%s} [%s] Completed: %d %s [%d bytes] %s %s",
  1242. request.txn_id,
  1243. request.destination,
  1244. response.code,
  1245. response.phrase.decode("ascii", errors="replace"),
  1246. length,
  1247. request.method,
  1248. request.uri.decode("ascii"),
  1249. )
  1250. return length, headers
  1251. def _flatten_response_never_received(e: BaseException) -> str:
  1252. if hasattr(e, "reasons"):
  1253. reasons = ", ".join(
  1254. _flatten_response_never_received(f.value) for f in e.reasons
  1255. )
  1256. return "%s:[%s]" % (type(e).__name__, reasons)
  1257. else:
  1258. return repr(e)
  1259. def check_content_type_is(headers: Headers, expected_content_type: str) -> None:
  1260. """
  1261. Check that a set of HTTP headers have a Content-Type header, and that it
  1262. is the expected value..
  1263. Args:
  1264. headers: headers to check
  1265. Raises:
  1266. RequestSendFailed: if the Content-Type header is missing or doesn't match
  1267. """
  1268. content_type_headers = headers.getRawHeaders(b"Content-Type")
  1269. if content_type_headers is None:
  1270. raise RequestSendFailed(
  1271. RuntimeError("No Content-Type header received from remote server"),
  1272. can_retry=False,
  1273. )
  1274. c_type = content_type_headers[0].decode("ascii") # only the first header
  1275. val, options = cgi.parse_header(c_type)
  1276. if val != expected_content_type:
  1277. raise RequestSendFailed(
  1278. RuntimeError(
  1279. f"Remote server sent Content-Type header of '{c_type}', not '{expected_content_type}'",
  1280. ),
  1281. can_retry=False,
  1282. )