/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use std::{marker::PhantomData, ops::ControlFlow, sync::Arc, time::Duration}; use ews::{OperationResponse, ResponseClass, response::ResponseError, soap}; use moz_http::{Response, StatusCode}; use protocol_shared::operation_sender::ResponseProcessor; use crate::{error::XpComEwsError, server_version::ServerVersionHandler}; /// A [`ResponseProcessor`] for an EWS response. pub(crate) struct EwsResponseProcessor { // We need a handle on the version handler to record the server version // that's located in the response. version_handler: Arc, // We need to parameterize `EwsResponseProcessor` on the concrete response // type to set the `ResponseProcessor::ReturnValue` associated type. resp_type: PhantomData, } impl EwsResponseProcessor { pub fn new(version_handler: Arc) -> EwsResponseProcessor { EwsResponseProcessor { version_handler, resp_type: PhantomData, } } } impl ResponseProcessor for EwsResponseProcessor where RespT: OperationResponse, { type ReturnValue = RespT; type Error = XpComEwsError; fn handles_error_status(&self, status: StatusCode) -> bool { // EWS does not indicate things like rate-limiting in HTTP status codes; // it typically responds with a 500 error that we need to parse to // figure out why our request is being rejected. status.0 == 500 } async fn check_response_for_error( &mut self, name: &str, resp: Response, ) -> Result, Self::Error> { let op_result: Result, _> = soap::Envelope::from_xml_document(resp.body()); match op_result { Ok(envelope) => { // If the server responded with a version identifier, store // it so we can use it later. if let Some(header) = envelope .headers .into_iter() // Filter out headers we don't care about. .find_map(|hdr| match hdr { soap::Header::ServerVersionInfo(server_version_info) => { Some(server_version_info) } _ => None, }) { self.version_handler.update_server_version(header)?; } // Check if the first response is a back off message, and // retry if so. if let Some(ResponseClass::Error(ResponseError { message_xml: Some(ews::MessageXml::ServerBusy(server_busy)), .. })) = envelope.body.response_messages().first() { log::debug!("Rate-limit hit (from ResponseMessage): {name}"); let delay = Duration::from_millis(server_busy.back_off_milliseconds as u64); return Ok(ControlFlow::Continue(delay)); } log::debug!("Request is not being rate-limited: {name}"); Ok(ControlFlow::Break(envelope.body)) } Err(err) => { // Check first to see if the request has been throttled and // needs to be retried. let backoff_delay_ms = maybe_get_backoff_delay_ms(&err); if let Some(backoff_delay_ms) = backoff_delay_ms { log::debug!("Rate-limit hit (from Fault): {name}"); let delay = Duration::from_millis(backoff_delay_ms as u64); return Ok(ControlFlow::Continue(delay)); } // If not, propagate the error. Err(err.into()) } } } } /// Gets the time to wait before retrying a throttled request, if any. /// /// When an Exchange server throttles a request, the response will specify a /// delay which should be observed before the request is retried. fn maybe_get_backoff_delay_ms(err: &ews::Error) -> Option { if let ews::Error::RequestFault(fault) = err { // We successfully sent a request, but it was rejected for some reason. // Whatever the reason, retry if we're provided with a backoff delay. let message_xml = fault.as_ref().detail.as_ref()?.message_xml.as_ref()?; match message_xml { ews::MessageXml::ServerBusy(server_busy) => Some(server_busy.back_off_milliseconds), _ => None, } } else { None } }