use std::{
cell::Cell,
fmt, io,
io::{IoSlice, Read, Write},
net::{
IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
},
string::{String, ToString},
sync::{
Arc, Condvar, Mutex,
atomic::{AtomicBool, Ordering},
},
thread,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
vec::Vec,
};
use crate::{
BodyPlan, ClientConn, ClientEvent, DateCache, HttpDate, HttpError, Limits, Method, Progress,
Request, RequestHead, Response, ServerConn, ServerEvent,
message::OutBody,
redact::{FieldValue, serialized_fields},
};
const DRAIN_LIMIT: usize = 256 * 1024;
const DRAIN_READ_TIMEOUT: Duration = Duration::from_millis(250);
const DRAIN_TOTAL: Duration = Duration::from_secs(1);
const OUT_RETAIN: usize = 64 * 1024;
const VECTORED_BODY: usize = 4 * 1024;
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Http(HttpError),
InvalidUrl(&'static str),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::Http(e) => write!(f, "HTTP error: {e}"),
Self::InvalidUrl(why) => write!(f, "invalid URL: {why}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::Http(e) => Some(e),
Self::InvalidUrl(_) => None,
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
impl From<HttpError> for Error {
fn from(e: HttpError) -> Self {
Self::Http(e)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ServerConfig {
limits: Limits,
max_connections: usize,
read_timeout: Option<Duration>,
request_timeout: Option<Duration>,
write_timeout: Option<Duration>,
response_deadline: Option<Duration>,
max_requests: Option<u64>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self::new()
}
}
impl ServerConfig {
#[must_use]
pub const fn new() -> Self {
Self {
limits: Limits::new(),
max_connections: 64,
read_timeout: Some(Duration::from_secs(10)),
request_timeout: Some(Duration::from_secs(30)),
write_timeout: Some(Duration::from_secs(10)),
response_deadline: Some(Duration::from_secs(60)),
max_requests: None,
}
}
#[must_use]
pub const fn with_limits(mut self, limits: Limits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub const fn with_max_connections(mut self, n: usize) -> Self {
self.max_connections = if n == 0 { 1 } else { n };
self
}
#[must_use]
pub const fn with_read_timeout(mut self, timeout: Option<Duration>) -> Self {
self.read_timeout = timeout;
self
}
#[must_use]
pub const fn with_request_timeout(mut self, timeout: Option<Duration>) -> Self {
self.request_timeout = timeout;
self
}
#[must_use]
pub const fn with_write_timeout(mut self, timeout: Option<Duration>) -> Self {
self.write_timeout = timeout;
self
}
#[must_use]
pub const fn with_response_deadline(mut self, deadline: Option<Duration>) -> Self {
self.response_deadline = deadline;
self
}
#[must_use]
pub const fn with_max_requests_per_connection(mut self, max: Option<u64>) -> Self {
self.max_requests = match max {
Some(0) => Some(1),
other => other,
};
self
}
#[must_use]
pub const fn limits(&self) -> Limits {
self.limits
}
#[must_use]
pub const fn max_connections(&self) -> usize {
self.max_connections
}
#[must_use]
pub const fn read_timeout(&self) -> Option<Duration> {
self.read_timeout
}
#[must_use]
pub const fn request_timeout(&self) -> Option<Duration> {
self.request_timeout
}
#[must_use]
pub const fn write_timeout(&self) -> Option<Duration> {
self.write_timeout
}
#[must_use]
pub const fn response_deadline(&self) -> Option<Duration> {
self.response_deadline
}
#[must_use]
pub const fn max_requests_per_connection(&self) -> Option<u64> {
self.max_requests
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Upgraded {
pub stream: TcpStream,
pub leftover: Vec<u8>,
pub target: String,
}
type UpgradeHook = Arc<dyn Fn(Upgraded) + Send + Sync>;
pub struct Server {
listener: TcpListener,
local: SocketAddr,
config: ServerConfig,
stop: Arc<AtomicBool>,
upgrade: Option<UpgradeHook>,
}
impl fmt::Debug for Server {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Server")
.field("local_addr", &self.local)
.field("config", &self.config)
.finish()
}
}
#[derive(Clone, Debug)]
pub struct ShutdownHandle {
stop: Arc<AtomicBool>,
wake: SocketAddr,
}
impl ShutdownHandle {
pub fn shutdown(&self) {
self.stop.store(true, Ordering::SeqCst);
if let Ok(stream) = TcpStream::connect_timeout(&self.wake, Duration::from_secs(1)) {
drop(stream);
}
}
}
#[derive(Default)]
struct Active {
count: Mutex<usize>,
idle: Condvar,
}
impl Active {
fn try_enter(&self, max: usize) -> bool {
let mut n = self
.count
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if *n >= max {
return false;
}
*n += 1;
true
}
fn leave(&self) {
let mut n = self
.count
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*n = n.saturating_sub(1);
if *n == 0 {
self.idle.notify_all();
}
}
fn wait_idle(&self) {
let mut n = self
.count
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while *n > 0 {
n = self
.idle
.wait(n)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
}
struct ActiveGuard(Arc<Active>);
impl Drop for ActiveGuard {
fn drop(&mut self) {
self.0.leave();
}
}
impl Server {
pub fn bind(addr: impl ToSocketAddrs) -> io::Result<Self> {
let listener = TcpListener::bind(addr)?;
let local = listener.local_addr()?;
Ok(Self {
listener,
local,
config: ServerConfig::new(),
stop: Arc::new(AtomicBool::new(false)),
upgrade: None,
})
}
#[must_use]
pub fn with_config(mut self, config: ServerConfig) -> Self {
self.config = config;
self
}
#[must_use]
pub fn on_upgrade(mut self, hook: impl Fn(Upgraded) + Send + Sync + 'static) -> Self {
self.upgrade = Some(Arc::new(hook));
self
}
#[must_use]
pub fn local_addr(&self) -> SocketAddr {
self.local
}
#[must_use]
pub fn shutdown_handle(&self) -> ShutdownHandle {
let mut wake = self.local;
if wake.ip().is_unspecified() {
wake.set_ip(match wake.ip() {
IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::LOCALHOST),
IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::LOCALHOST),
});
}
ShutdownHandle {
stop: Arc::clone(&self.stop),
wake,
}
}
pub fn serve<H>(self, handler: H) -> io::Result<()>
where
H: Fn(&RequestHead<'_>, &[u8]) -> Response + Send + Sync + 'static,
{
let handler = Arc::new(handler);
let active = Arc::new(Active::default());
let mut backoff = AcceptBackoff::default();
for incoming in self.listener.incoming() {
if self.stop.load(Ordering::SeqCst) {
break;
}
let stream = match incoming {
Ok(stream) => {
backoff.success();
stream
}
Err(e) => {
thread::sleep(backoff.failure(e)?);
continue;
}
};
if !active.try_enter(self.config.max_connections) {
refuse_busy(stream, &self.config);
continue;
}
let guard = ActiveGuard(Arc::clone(&active));
let handler = Arc::clone(&handler);
let upgrade = self.upgrade.clone();
let stop = Arc::clone(&self.stop);
let config = self.config;
let spawned = thread::Builder::new()
.name("http-net-conn".into())
.spawn(move || {
let _guard = guard;
serve_stream(stream, &config, &*handler, upgrade.as_deref(), &stop);
});
drop(spawned);
}
active.wait_idle();
Ok(())
}
}
#[derive(Debug, Default)]
struct AcceptBackoff {
consecutive: u32,
}
impl AcceptBackoff {
const WAIT: Duration = Duration::from_millis(10);
const LIMIT: u32 = 1000;
fn success(&mut self) {
self.consecutive = 0;
}
fn failure(&mut self, error: io::Error) -> io::Result<Duration> {
self.consecutive += 1;
if self.consecutive > Self::LIMIT {
Err(error)
} else {
Ok(Self::WAIT)
}
}
}
fn refuse_busy(mut stream: TcpStream, config: &ServerConfig) {
const BUSY: &[u8] =
b"HTTP/1.1 503 Service Unavailable\r\ncontent-length: 0\r\nconnection: close\r\n\r\n";
if stream.set_write_timeout(config.write_timeout).is_err()
|| stream.write_all(BUSY).is_err()
|| stream.shutdown(Shutdown::Write).is_err()
|| stream.set_nonblocking(true).is_err()
{
return;
}
let mut scratch = [0u8; 4096];
let mut drained = 0;
while drained < DRAIN_LIMIT {
match stream.read(&mut scratch) {
Ok(n) if n > 0 => drained += n,
_ => break,
}
}
}
fn serve_stream(
stream: TcpStream,
config: &ServerConfig,
handler: &(dyn Fn(&RequestHead<'_>, &[u8]) -> Response + Send + Sync),
upgrade: Option<&(dyn Fn(Upgraded) + Send + Sync)>,
stop: &AtomicBool,
) {
let configured = stream.set_read_timeout(config.read_timeout).is_ok()
&& stream.set_write_timeout(config.write_timeout).is_ok()
&& stream.set_nodelay(true).is_ok();
if !configured {
return;
}
let mut conn = Connection::new(stream, config.limits)
.with_request_timeout(config.request_timeout)
.with_response_deadline(config.response_deadline);
conn.refuse_unknown_expectations = true;
while !stop.load(Ordering::SeqCst) {
let outcome = match conn.next_request() {
Ok(Some(incoming)) => {
let head = incoming.head();
Ok((
handler(&head, incoming.body()),
head.method() == Method::Head,
))
}
Ok(None) => break,
Err(e) => Err(e),
};
let (response, is_head) = match outcome {
Ok(answer) => answer,
Err(Error::Http(e)) => {
if conn.respond(&Response::from_error(&e)).is_err() {
return;
}
break;
}
Err(Error::Io(e)) if is_timeout(&e) && conn.request_in_progress() => {
if conn.send_request_timeout().is_ok() {
conn.close();
}
return;
}
Err(_) => return,
};
let streamed = response.body_bytes().is_none();
let response = if streamed && !is_head {
Response::text(
500,
"handler error: Server cannot send a streamed response; \
use Connection for streaming",
)
.close()
} else {
response
};
if config
.max_requests
.is_some_and(|max| conn.conn.requests_served().saturating_add(1) >= max)
{
conn.conn.set_keep_alive(false);
}
match conn.respond(&response) {
Ok(()) if streamed && is_head => {
if conn.end_body().is_err() {
return;
}
}
Ok(()) => {}
Err(Error::Http(e)) => {
let message = format!("handler error: invalid response: {e}");
if conn
.respond(&Response::text(500, &message).close())
.is_err()
{
return;
}
}
Err(_) => return,
}
if conn.is_upgraded() {
match (conn.into_upgraded(), upgrade) {
(Some(up), Some(hook)) => hook(up),
(Some(up), None) => drain_and_close(up.stream),
(None, _) => {}
}
return;
}
}
conn.close();
}
fn drain_and_close(mut stream: TcpStream) {
if stream.flush().is_err() || stream.shutdown(Shutdown::Write).is_err() {
return;
}
let deadline = Instant::now() + DRAIN_TOTAL;
let mut scratch = [0u8; 4096];
let mut drained = 0;
while drained < DRAIN_LIMIT {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero()
|| stream
.set_read_timeout(Some(left.min(DRAIN_READ_TIMEOUT)))
.is_err()
{
break;
}
match stream.read(&mut scratch) {
Ok(0) | Err(_) => break,
Ok(n) => drained += n,
}
}
}
fn response_timed_out() -> Error {
Error::Io(io::Error::new(
io::ErrorKind::TimedOut,
"the response was not written within the response deadline",
))
}
fn request_timed_out() -> Error {
Error::Io(io::Error::new(
io::ErrorKind::TimedOut,
"the request did not arrive within the request timeout",
))
}
fn is_timeout(e: &io::Error) -> bool {
matches!(
e.kind(),
io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock
)
}
#[derive(Debug)]
pub struct Incoming<'a> {
head: RequestHead<'a>,
body: &'a [u8],
}
impl<'a> Incoming<'a> {
#[must_use]
pub fn head(&self) -> RequestHead<'a> {
self.head
}
#[must_use]
pub fn body(&self) -> &'a [u8] {
self.body
}
}
pub struct Connection {
stream: TcpStream,
conn: ServerConn,
body: Vec<u8>,
out: Vec<u8>,
read_timeout: Option<Duration>,
request_timeout: Option<Duration>,
deadline: Option<Instant>,
shortened: bool,
write_timeout: Option<Duration>,
response_deadline: Option<Duration>,
write_deadline: Option<Instant>,
write_shortened: bool,
refuse_unknown_expectations: bool,
}
impl fmt::Debug for Connection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Connection")
.field("stream", &self.stream)
.field("conn", &self.conn)
.field("body_len", &self.body.len())
.field("out_len", &self.out.len())
.field("read_timeout", &self.read_timeout)
.field("request_timeout", &self.request_timeout)
.field("write_timeout", &self.write_timeout)
.field("response_deadline", &self.response_deadline)
.finish_non_exhaustive()
}
}
impl Connection {
#[must_use]
pub fn new(stream: TcpStream, limits: Limits) -> Self {
let read_timeout = stream.read_timeout().ok().flatten();
let write_timeout = stream.write_timeout().ok().flatten();
Self {
stream,
conn: ServerConn::with_limits(limits),
body: Vec::new(),
out: Vec::new(),
read_timeout,
request_timeout: None,
deadline: None,
shortened: false,
write_timeout,
response_deadline: None,
write_deadline: None,
write_shortened: false,
refuse_unknown_expectations: false,
}
}
#[must_use]
pub fn with_response_deadline(mut self, deadline: Option<Duration>) -> Self {
self.response_deadline = deadline;
self
}
#[must_use]
pub fn with_request_timeout(mut self, timeout: Option<Duration>) -> Self {
self.request_timeout = timeout;
self
}
fn request_in_progress(&self) -> bool {
self.conn.request().is_some() || !self.conn.remaining_input().is_empty()
}
fn send_request_timeout(&mut self) -> Result<(), Error> {
if self.conn.response_started() {
return Err(Error::Http(HttpError::InvalidState(
"a response was already started",
)));
}
const BODY: &[u8] = b"request timed out";
self.out.clear();
self.out.extend_from_slice(
b"HTTP/1.1 408 Request Timeout\r\ncontent-type: text/plain; charset=utf-8\r\n\
content-length: ",
);
self.out
.extend_from_slice(BODY.len().to_string().as_bytes());
self.out
.extend_from_slice(b"\r\nconnection: close\r\ndate: ");
self.out.extend_from_slice(now_date().as_bytes());
self.out.extend_from_slice(b"\r\n\r\n");
self.out.extend_from_slice(BODY);
self.flush_out()
}
fn fill(&mut self) -> Result<(), Error> {
if let Some(limit) = self.request_timeout {
if self.deadline.is_none() && self.request_in_progress() {
self.deadline = Some(Instant::now() + limit);
}
}
let mut at_deadline = false;
match self.deadline {
Some(deadline) => {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(request_timed_out());
}
let wait = self.read_timeout.map_or(left, |t| t.min(left));
at_deadline = wait == left;
self.stream.set_read_timeout(Some(wait))?;
self.shortened = true;
}
None if self.shortened => {
self.stream.set_read_timeout(self.read_timeout)?;
self.shortened = false;
}
None => {}
}
let spare = self.conn.read_buf();
if spare.is_empty() {
return Err(Error::Http(HttpError::InvalidState(
"input buffer full while the parser needs input",
)));
}
let n = loop {
match self.stream.read(spare) {
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) if at_deadline && is_timeout(&e) => return Err(request_timed_out()),
other => break other?,
}
};
if n == 0 {
self.conn.feed_eof();
} else {
self.conn.commit(n);
}
Ok(())
}
pub fn next_request(&mut self) -> Result<Option<Incoming<'_>>, Error> {
self.body.clear();
self.deadline = None;
loop {
match self.conn.poll()? {
ServerEvent::Request(head) => {
let unknown = head.has_unknown_expectation();
let expects_continue = head.expects_continue();
if self.refuse_unknown_expectations && unknown {
let refusal =
Response::text(417, "expectation not supported; send without Expect")
.close();
self.respond(&refusal)?;
return Ok(None);
}
if expects_continue {
self.out.clear();
self.conn.send_continue(&mut self.out)?;
self.flush_out()?;
}
}
ServerEvent::Body(chunk) => self.body.extend_from_slice(chunk),
ServerEvent::RequestEnd => break,
ServerEvent::NeedInput => self.fill()?,
ServerEvent::AwaitingResponse => {
return Err(Error::Http(HttpError::InvalidState(
"respond to the previous request first",
)));
}
_ => return Ok(None),
}
}
match self.conn.request() {
Some(head) => Ok(Some(Incoming {
head,
body: &self.body,
})),
None => Ok(None),
}
}
pub fn respond(&mut self, response: &Response) -> Result<(), Error> {
self.out.clear();
let date = now_date();
let tail = match response.body_bytes() {
Some(body) if body.len() > VECTORED_BODY => {
let plan = self.conn.respond_head_dated(
response,
Some(&date.as_bytes()[..]),
&mut self.out,
)?;
if plan == BodyPlan::Raw { body } else { &[][..] }
}
_ => {
self.conn
.respond_with_date(response, &date, &mut self.out)?;
&[][..]
}
};
self.write_deadline = self.response_deadline.map(|d| Instant::now() + d);
self.flush_with(tail)
}
pub fn send_informational(&mut self, response: &Response) -> Result<(), Error> {
self.out.clear();
self.conn.send_informational(response, &mut self.out)?;
self.flush_out()
}
pub fn send_body(&mut self, data: &[u8]) -> Result<(), Error> {
self.out.clear();
self.conn.send_body(data, &mut self.out)?;
self.flush_out()
}
pub fn end_body(&mut self) -> Result<(), Error> {
self.out.clear();
self.conn.end_body(&mut self.out)?;
self.flush_out()
}
fn flush_out(&mut self) -> Result<(), Error> {
self.flush_with(&[])
}
fn flush_with(&mut self, tail: &[u8]) -> Result<(), Error> {
let head_len = self.out.len();
let total = head_len + tail.len();
let mut written = 0;
while written < total {
if let Some(deadline) = self.write_deadline {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(response_timed_out());
}
let wait = self.write_timeout.map_or(left, |t| t.min(left));
self.stream.set_write_timeout(Some(wait))?;
self.write_shortened = true;
}
let result = if written >= head_len {
self.stream.write(&tail[written - head_len..])
} else if tail.is_empty() {
self.stream.write(&self.out[written..])
} else {
let parts = [IoSlice::new(&self.out[written..]), IoSlice::new(tail)];
self.stream.write_vectored(&parts)
};
match result {
Ok(0) => return Err(Error::Io(io::ErrorKind::WriteZero.into())),
Ok(n) => written += n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e)
if is_timeout(&e)
&& self.write_deadline.is_some_and(|d| Instant::now() >= d) =>
{
return Err(response_timed_out());
}
Err(e) => return Err(Error::Io(e)),
}
}
if self.conn.progress() != Progress::WritingBody {
self.write_deadline = None;
if self.write_shortened {
self.stream.set_write_timeout(self.write_timeout)?;
self.write_shortened = false;
}
}
if self.out.capacity() > OUT_RETAIN {
self.out = Vec::new();
}
Ok(())
}
#[must_use]
pub fn is_upgraded(&self) -> bool {
self.conn.is_upgraded()
}
#[must_use]
pub fn into_upgraded(self) -> Option<Upgraded> {
if !self.is_upgraded() {
return None;
}
let target = self
.conn
.request()
.map(|r| r.target().to_string())
.unwrap_or_default();
if self.shortened {
let _ = self.stream.set_read_timeout(self.read_timeout);
}
Some(Upgraded {
leftover: self.conn.remaining_input().to_vec(),
target,
stream: self.stream,
})
}
pub fn close(self) {
drain_and_close(self.stream);
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct ClientResponse {
status: u16,
headers: Vec<(String, Vec<u8>)>,
body: Vec<u8>,
}
impl fmt::Debug for ClientResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct Fields<'a>(&'a [(String, Vec<u8>)]);
impl fmt::Debug for Fields<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.0.iter().map(|(name, value)| {
(
name.as_str(),
FieldValue {
name: name.as_bytes(),
value,
},
)
}))
.finish()
}
}
f.debug_struct("ClientResponse")
.field("status", &self.status)
.field("headers", &Fields(&self.headers))
.field("body", &format_args!("{} bytes", self.body.len()))
.finish()
}
}
impl ClientResponse {
#[must_use]
pub fn status(&self) -> u16 {
self.status
}
pub fn headers(&self) -> impl Iterator<Item = (&str, &[u8])> {
self.headers.iter().map(|(n, v)| (n.as_str(), v.as_slice()))
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&[u8]> {
self.headers
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_slice())
}
#[must_use]
pub fn body(&self) -> &[u8] {
&self.body
}
#[must_use]
pub fn text(&self) -> Option<&str> {
core::str::from_utf8(&self.body).ok()
}
#[must_use]
pub fn into_body(self) -> Vec<u8> {
self.body
}
}
pub fn get(url: &str) -> Result<ClientResponse, Error> {
Client::new().get(url)
}
pub fn post(url: &str, content_type: &str, body: &[u8]) -> Result<ClientResponse, Error> {
let request = Request::post("/")
.header("content-type", content_type)
.body(body.to_vec());
Client::new().request(url, request)
}
pub fn serve<H>(addr: impl ToSocketAddrs, handler: H) -> io::Result<()>
where
H: Fn(&RequestHead<'_>, &[u8]) -> Response + Send + Sync + 'static,
{
Server::bind(addr)?.serve(handler)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Client {
limits: Limits,
timeout: Duration,
deadline: Option<Duration>,
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
impl Client {
#[must_use]
pub const fn new() -> Self {
Self {
limits: Limits::new().with_max_body(16 * 1024 * 1024),
timeout: Duration::from_secs(30),
deadline: None,
}
}
#[must_use]
pub const fn with_limits(mut self, limits: Limits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub const fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = if timeout.is_zero() {
Duration::from_millis(1)
} else {
timeout
};
self
}
#[must_use]
pub const fn with_deadline(mut self, deadline: Option<Duration>) -> Self {
self.deadline = deadline;
self
}
pub fn get(&self, url: &str) -> Result<ClientResponse, Error> {
self.request(url, Request::get("/"))
}
pub fn request(&self, url: &str, request: Request) -> Result<ClientResponse, Error> {
let url = Url::parse(url)?;
let request = retarget(&request, url.target, url.authority)?;
let deadline = self.deadline.map(|d| Instant::now() + d);
let addrs = (url.host, url.port).to_socket_addrs()?;
let mut stream = connect_any(addrs, self.timeout, deadline)?;
let mut conn = ClientConn::with_limits(self.limits);
let mut out = Vec::new();
conn.send_request(&request, &mut out)?;
let mut io = Timed {
stream: &mut stream,
timeout: self.timeout,
deadline,
};
io.write_all(&out)?;
read_response(&mut io, &mut conn)
}
}
fn retarget(request: &Request, target: &str, authority: &str) -> Result<Request, Error> {
if let Some(e) = request.error() {
return Err(Error::Http(e));
}
if request.has_host {
return Err(Error::Http(HttpError::InvalidHost));
}
if request.upgrade {
return Err(Error::Http(HttpError::InvalidState(
"the blocking client cannot hand over an upgraded connection; use ClientConn",
)));
}
let mut rebuilt = Request::new(request.method(), target).header("host", authority);
for (name, value) in serialized_fields(request.fields()) {
let name = core::str::from_utf8(name).unwrap_or("");
if request.expect_continue
&& name.eq_ignore_ascii_case("expect")
&& value.eq_ignore_ascii_case(b"100-continue")
{
continue;
}
rebuilt = rebuilt.header(name, value);
}
if request.expect_continue {
rebuilt = rebuilt.expect_continue();
}
rebuilt = match &request.body {
OutBody::Full(body) => rebuilt.body(body.clone()),
OutBody::Length(_) | OutBody::Chunked => {
return Err(Error::Http(HttpError::InvalidState(
"the blocking client sends full bodies only; use ClientConn to stream",
)));
}
};
let rebuilt = rebuilt.close();
match rebuilt.error() {
Some(e) => Err(Error::Http(e)),
None => Ok(rebuilt),
}
}
fn connect_any(
addrs: impl IntoIterator<Item = SocketAddr>,
timeout: Duration,
deadline: Option<Instant>,
) -> Result<TcpStream, Error> {
let mut last = None;
for addr in addrs {
let wait = match deadline {
Some(d) => {
let left = d.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(exchange_timed_out());
}
left.min(timeout)
}
None => timeout,
};
match TcpStream::connect_timeout(&addr, wait) {
Ok(stream) => {
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
stream.set_nodelay(true)?;
return Ok(stream);
}
Err(e) => last = Some(e),
}
}
Err(Error::Io(last.unwrap_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "the host resolved to no address")
})))
}
fn exchange_timed_out() -> Error {
Error::Io(io::Error::new(
io::ErrorKind::TimedOut,
"the exchange did not finish within the client deadline",
))
}
struct Timed<'a> {
stream: &'a mut TcpStream,
timeout: Duration,
deadline: Option<Instant>,
}
impl Timed<'_> {
fn arm(&mut self) -> Result<(), Error> {
if let Some(d) = self.deadline {
let left = d.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(exchange_timed_out());
}
let wait = left.min(self.timeout);
self.stream.set_read_timeout(Some(wait))?;
self.stream.set_write_timeout(Some(wait))?;
}
Ok(())
}
fn check(&self, e: io::Error) -> Error {
if is_timeout(&e) && self.deadline.is_some_and(|d| Instant::now() >= d) {
exchange_timed_out()
} else {
Error::Io(e)
}
}
fn write_all(&mut self, mut data: &[u8]) -> Result<(), Error> {
while !data.is_empty() {
self.arm()?;
match self.stream.write(data) {
Ok(0) => return Err(Error::Io(io::ErrorKind::WriteZero.into())),
Ok(n) => data = &data[n..],
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(self.check(e)),
}
}
Ok(())
}
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
loop {
self.arm()?;
match self.stream.read(buf) {
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(self.check(e)),
Ok(n) => return Ok(n),
}
}
}
}
fn read_response(stream: &mut Timed<'_>, conn: &mut ClientConn) -> Result<ClientResponse, Error> {
let mut response = ClientResponse {
status: 0,
headers: Vec::new(),
body: Vec::new(),
};
loop {
match conn.poll()? {
ClientEvent::Response(head) => {
response.status = head.status();
response.headers = head
.headers()
.map(|(n, v)| (n.to_string(), v.to_vec()))
.collect();
}
ClientEvent::Body(chunk) => response.body.extend_from_slice(chunk),
ClientEvent::ResponseEnd => return Ok(response),
ClientEvent::NeedInput => {
let spare = conn.read_buf();
if spare.is_empty() {
return Err(Error::Http(HttpError::InvalidState(
"input buffer full while the parser needs input",
)));
}
let n = stream.read(spare)?;
if n == 0 {
conn.feed_eof();
} else {
conn.commit(n);
}
}
_ => {
return Err(Error::Http(HttpError::InvalidState(
"the connection ended without a complete response",
)));
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct Url<'a> {
authority: &'a str,
host: &'a str,
port: u16,
target: &'a str,
}
impl<'a> Url<'a> {
fn parse(url: &'a str) -> Result<Self, Error> {
let scheme_end = url.find("://").ok_or(Error::InvalidUrl("missing scheme"))?;
if !url[..scheme_end].eq_ignore_ascii_case("http") {
return Err(Error::InvalidUrl(
"only http:// is supported (no TLS in this crate)",
));
}
let rest = &url[scheme_end + 3..];
let rest = rest.split_once('#').map_or(rest, |(r, _)| r);
let auth_end = rest.find(['/', '?']).unwrap_or(rest.len());
let authority = &rest[..auth_end];
let target = match &rest[auth_end..] {
"" => "/",
t if t.starts_with('?') => {
return Err(Error::InvalidUrl("a query needs a path, for example /?q"));
}
t => t,
};
if authority.is_empty() {
return Err(Error::InvalidUrl("missing host"));
}
if authority.contains('@') {
return Err(Error::InvalidUrl(
"credentials in the URL are not supported",
));
}
let (host, port) = if let Some(v6) = authority.strip_prefix('[') {
let (inside, after) = v6.split_once(']').ok_or(Error::InvalidUrl("unclosed ["))?;
let port = match after {
"" => None,
p => Some(
p.strip_prefix(':')
.ok_or(Error::InvalidUrl("junk after ]"))?,
),
};
(inside, port)
} else {
match authority.rsplit_once(':') {
Some((h, p)) => (h, Some(p)),
None => (authority, None),
}
};
let port = match port {
None => 80,
Some(p) => p.parse().map_err(|_| Error::InvalidUrl("bad port"))?,
};
if host.is_empty() {
return Err(Error::InvalidUrl("missing host"));
}
Ok(Self {
authority,
host,
port,
target,
})
}
}
thread_local! {
static DATE: Cell<DateCache> = const { Cell::new(DateCache::new()) };
}
#[must_use]
pub fn now_date() -> HttpDate {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
DATE.with(|cell| {
let mut cache = cell.get();
let date = *cache.update(secs);
cell.set(cache);
date
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_accept_errors_back_off_and_end_after_the_limit() {
let error = || io::Error::from(io::ErrorKind::ConnectionAborted);
let mut backoff = AcceptBackoff::default();
for _ in 0..AcceptBackoff::LIMIT {
let wait = backoff.failure(error()).expect("still tolerated");
assert!(wait >= Duration::from_millis(10), "a failure must wait");
}
let last = backoff.failure(error()).expect_err("the limit is reached");
assert_eq!(last.kind(), io::ErrorKind::ConnectionAborted);
let mut backoff = AcceptBackoff::default();
for _ in 0..3 {
for _ in 0..AcceptBackoff::LIMIT {
assert!(backoff.failure(error()).is_ok());
}
backoff.success();
}
assert!(
backoff.failure(error()).is_ok(),
"a success resets the count"
);
}
#[test]
fn test_now_date_is_the_current_second() {
let before = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let shown = now_date();
let after = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
assert!(
shown == HttpDate::from_unix_seconds(before)
|| shown == HttpDate::from_unix_seconds(after),
"{shown}"
);
assert_eq!(now_date().as_bytes().len(), 29);
}
#[test]
fn test_connect_any_unreachable_first_address_tries_the_next() {
let closed = TcpListener::bind("127.0.0.1:0").unwrap();
let refused = closed.local_addr().unwrap();
drop(closed);
let open = TcpListener::bind("127.0.0.1:0").unwrap();
let good = open.local_addr().unwrap();
let stream = connect_any([refused, good], Duration::from_secs(5), None).unwrap();
assert_eq!(stream.peer_addr().unwrap(), good);
let err = connect_any([refused], Duration::from_secs(5), None).unwrap_err();
assert!(matches!(err, Error::Io(_)), "{err}");
let err = connect_any(Vec::<SocketAddr>::new(), Duration::from_secs(5), None).unwrap_err();
assert!(
matches!(&err, Error::Io(e) if e.kind() == io::ErrorKind::NotFound),
"{err}"
);
}
#[test]
fn test_connect_any_past_the_deadline_times_out() {
let open = TcpListener::bind("127.0.0.1:0").unwrap();
let good = open.local_addr().unwrap();
let err = connect_any([good], Duration::from_secs(5), Some(Instant::now())).unwrap_err();
assert!(
matches!(&err, Error::Io(e) if e.kind() == io::ErrorKind::TimedOut),
"{err}"
);
}
#[test]
fn test_client_request_with_host_is_refused() {
let req = Request::get("/").header("host", "a");
let err = Client::new()
.request("http://127.0.0.1:1/", req)
.unwrap_err();
assert!(matches!(err, Error::Http(HttpError::InvalidHost)), "{err}");
}
#[test]
fn test_client_request_streamed_body_is_refused() {
for req in [
Request::post("/").chunked(),
Request::put("/").content_length(3),
] {
let err = Client::new()
.request("http://127.0.0.1:1/", req)
.unwrap_err();
assert!(
matches!(err, Error::Http(HttpError::InvalidState(_))),
"{err}"
);
}
}
#[test]
fn test_retarget_keeps_method_fields_body_and_flags() {
let original = Request::new("PATCH", "/ignored")
.header("x-a", "1")
.header("content-type", "text/plain")
.body(&b"xyz"[..]);
let r = retarget(&original, "/real?q", "example.com:8080").unwrap();
assert_eq!(r.method(), "PATCH");
assert_eq!(r.target(), "/real?q");
assert_eq!(r.header_value("host"), Some(&b"example.com:8080"[..]));
assert_eq!(r.header_value("x-a"), Some(&b"1"[..]));
assert_eq!(r.body_bytes(), Some(&b"xyz"[..]));
let mut out = Vec::new();
ClientConn::new().send_request(&r, &mut out).unwrap();
let text = String::from_utf8(out).unwrap();
assert!(
text.starts_with("PATCH /real?q HTTP/1.1\r\nhost: example.com:8080\r\nx-a: 1\r\n"),
"{text}"
);
assert!(text.contains("connection: close\r\n"), "{text}");
assert!(text.ends_with("\r\n\r\nxyz"), "{text}");
let up = Request::get("/").upgrade("websocket");
assert!(matches!(
retarget(&up, "/", "a"),
Err(Error::Http(HttpError::InvalidState(_)))
));
let bad = Request::get("/").header("bad name", "x");
assert!(matches!(
retarget(&bad, "/", "a"),
Err(Error::Http(HttpError::InvalidHeaderName))
));
}
#[test]
fn test_url_parsing() {
let u = Url::parse("http://127.0.0.1:8080/a/b?c=d#frag").unwrap();
assert_eq!(
u,
Url {
authority: "127.0.0.1:8080",
host: "127.0.0.1",
port: 8080,
target: "/a/b?c=d"
}
);
let u = Url::parse("HTTP://[::1]/").unwrap();
assert_eq!((u.host, u.port, u.authority), ("::1", 80, "[::1]"));
assert_eq!(Url::parse("http://example.com").unwrap().target, "/");
for bad in [
"example.com",
"https://x/",
"http:///x",
"http://u:p@x/",
"http://x:99999/",
"http://[::1/",
"http://[::1]x/",
"http://x?q",
"http://:80/",
] {
assert!(
matches!(Url::parse(bad), Err(Error::InvalidUrl(_))),
"{bad}"
);
}
}
#[test]
fn test_error_display_and_source() {
use std::error::Error as _;
let e = Error::from(HttpError::BareCr);
assert!(e.to_string().starts_with("HTTP error"));
assert!(e.source().is_some());
let e = Error::from(io::Error::other("x"));
assert!(e.source().is_some());
assert!(Error::InvalidUrl("y").source().is_none());
}
}