diff --git a/src/lib.rs b/src/lib.rs index 96acd69..9bd962d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,7 @@ pub struct Server { listener: Arc, handler: Handler, timeout: Duration, + complex_timeout: Option, } impl Server { @@ -101,31 +102,116 @@ impl Server { }) .context("Request handler failed")?; - // Use a timeout for sending the response - let fut_send_and_flush = async { - send_response(response, &mut stream).await + self.send_response(response, &mut stream).await .context("Failed to send response")?; + Ok(()) + } + + async fn send_response(&self, mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { + let maybe_body = response.take_body(); + let header = response.header(); + + // Okay, I know this method looks really complicated, but I promise it's not. + // There's really only three things this method does: + // + // * Send the response header + // * Send the response body + // * Flush the stream + // + // All the other code is doing one of two things. Either it's + // + // * code to add and handle timeouts (that's what all the async blocks and calls + // to tokio::time::timeout are), or + // * logic to decide whether to use the special case timeout handling (seperate + // timeouts for the header and the body) vs the normal timeout handling (header, + // body, and flush all as one timeout) + // + // The split between the two cases happens at this very first if block. + // Everything in this deep chain of if's and if-let's is for the special case. If + // any one of the ifs fails, the code after the big if block is run, and that's + // the normal case. + // + // Hope this helps! Emi <3 + + if header.status == Status::SUCCESS && maybe_body.is_some() { + // aaaa let me have if-let chaining ;_; + if let "text/plain"|"text/gemini" = header.meta.as_str() { + if let Some(cplx_timeout) = self.complex_timeout { + + + ////////////// Use the special case timeout override ///////////////////////////// + + // Send the header & flush + let fut_send_header = async { + send_response_header(response.header(), stream).await + .context("Failed to write response header")?; + + stream.flush() + .await + .context("Failed to flush response header") + }; + tokio::time::timeout(self.timeout, fut_send_header) + .await + .context("Timed out while sending response header")??; + + // Send the body & flush + let fut_send_body = async { + send_response_body(maybe_body.unwrap(), stream).await + .context("Failed to write response body")?; + + stream.flush() + .await + .context("Failed to flush response body") + }; + tokio::time::timeout(cplx_timeout, fut_send_body) + .await + .context("Timed out while sending response body")??; + + return Ok(()) + } + } + } + + + ///////////// Use the normal timeout ///////////////////////////////////////////// + + let fut_send_response = async { + send_response_header(response.header(), stream).await + .context("Failed to write response header")?; + + if let Some(body) = maybe_body { + send_response_body(body, stream).await + .context("Failed to write response body")?; + } + stream.flush() .await .context("Failed to flush response data") }; - timeout(self.timeout, fut_send_and_flush) + tokio::time::timeout(self.timeout, fut_send_response) .await - .context("Client timed out receiving response data")??; + .context("Timed out while sending response data")??; Ok(()) + + ////////////////////////////////////////////////////////////////////////////////// } } pub struct Builder { addr: A, timeout: Duration, + complex_body_timeout_override: Option, } impl Builder { fn bind(addr: A) -> Self { - Self { addr, timeout: Duration::from_secs(30) } + Self { + addr, + timeout: Duration::from_secs(1), + complex_body_timeout_override: Some(Duration::from_secs(30)), + } } /// Set the timeout on incoming requests @@ -139,17 +225,54 @@ impl Builder { /// If you would like a timeout for your code itself, please use /// [`tokio::time::Timeout`] to implement it internally. /// - /// **The default timeout is 30 seconds.** If you are considering changing this, keep - /// in mind that some clients, when recieving a file type not supported for display, - /// will prompt the user how they would like to proceed. While this occurs, the - /// request hangs open. Setting a short timeout may close the prompt before user has - /// a chance to respond. If you are only serving `text/plain` and `text/gemini`, this - /// should not be a problem. + /// **The default timeout is 1 second.** As somewhat of a workaround for + /// shortcomings of the specification, this timeout, and any timeout set using this + /// method, is overridden in special cases, specifically for MIME types outside of + /// `text/plain` and `text/gemini`, to be 30 seconds. If you would like to change or + /// prevent this, please see + /// [`override_complex_body_timeout`](Self::override_complex_body_timeout()). pub fn set_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self } + /// Override the timeout for complex body types + /// + /// Many clients choose to handle body types which cannot be displayed by prompting + /// the user if they would like to download or open the request body. However, since + /// this prompt occurs in the middle of receiving a request, often the connection + /// times out before the end user is able to respond to the prompt. + /// + /// As a workaround, it is possible to set an override on the request timeout in + /// specific conditions: + /// + /// 1. **Only override the timeout for receiving the body of the request.** This will + /// not override the timeout on sending the request header, nor on receiving the + /// response header. + /// 2. **Only override the timeout for successful responses.** The only bodies which + /// have bodies are successful ones. In all other cases, there's no body to + /// timeout for + /// 3. **Only override the timeout for complex body types.** Almost all clients are + /// able to display `text/plain` and `text/gemini` responses, and will not prompt + /// the user for these response types. This means that there is no reason to + /// expect a client to have a human-length response time for these MIME types. + /// Because of this, responses of this type will not be overridden. + /// + /// This method is used to override the timeout for responses meeting these specific + /// criteria. All other stages of the connection will use the timeout specified in + /// [`set_timeout()`](Self::set_timeout()). + /// + /// If this is set to [`None`], then the client will have the default amount of time + /// to both receive the header and the body. If this is set to [`Some`], the client + /// will have the default amount of time to recieve the header, and an *additional* + /// alotment of time to recieve the body. + /// + /// The default timeout for this is 30 seconds. + pub fn override_complex_body_timeout(mut self, timeout: Option) -> Self { + self.complex_body_timeout_override = timeout; + self + } + pub async fn serve(self, handler: F) -> Result<()> where F: Fn(Request) -> HandlerResponse + Send + Sync + 'static, @@ -165,6 +288,7 @@ impl Builder { listener: Arc::new(listener), handler: Arc::new(handler), timeout: self.timeout, + complex_timeout: self.complex_body_timeout_override, }; server.serve().await @@ -199,18 +323,6 @@ async fn receive_request(stream: &mut (impl AsyncBufRead + Unpin)) -> Result Result<()> { - send_response_header(response.header(), stream).await - .context("Failed to send response header")?; - - if let Some(body) = response.take_body() { - send_response_body(body, stream).await - .context("Failed to send response body")?; - } - - Ok(()) -} - async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { let header = format!( "{status} {meta}\r\n",