Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Feedback on attempting to port to 3.0.0-rc2 #870

@sosthene-nitrokey

Description

@sosthene-nitrokey

Hi,

We are using ureq in our PKCS#11 module, thank you for this reliable and simple library.

We recently had to fork ureq in our use to implement missing functionality we wanted to have, mainly TCP keepalive and automatically discarding old connections.

We hope the 3.x.x branch allowing separate "connectors" and "transport" and the fact that it already supports a max age for idle connections would permit us to migrate back to upstream.

I attempted to port our implementation to the 3.0.0-rc2, here are the challenges I encountered:

Config fields are private

This prevents implementing some of the functionality of the upstream TcpConnector in our own implementation (for example there is no way to read the nodelay configuration of the config, preventing of implementing the same functionality in other crates. Looking at the main branch, it looks like this has been fixed, so this may be outdated.

There would be benefits to not boxing the connectors

In the connector traits, the transports are boxed, preventing chained connectors to make use of specificities of the previous transport. Ideally for example, it would be possible to use the specific type of the previous transport. For example a TCP keepalive connector could just wrap or come after a TcpTransport, get its TCP stream, and enable Keepalive on it.

The way I would do it would use associated types for the transports, and make chained and not chained connectors part of the trait.

pub trait Connector: Debug + Send + Sync + 'static {
    type Transport: Transport;

    fn boxed(self) -> AnyConnector
    where
        Self: Sized,
    {
        Box::new(self)
    }
    fn connect(&self, details: &ConnectionDetails) -> Result<Self::Transport, Error>;
}

pub trait AnyConnectorTrait: Debug + Send + Sync + 'static {
    fn connect(&self, details: &ConnectionDetails) -> Result<Box<dyn Transport>, Error>;
}

impl<C: Connector> AnyConnectorTrait for C {
    fn connect(&self, details: &ConnectionDetails) -> Result<Box<dyn Transport>, Error> {
        Ok(Box::new(self.connect(details)?))
    }
}

pub type AnyConnector = Box<dyn AnyConnectorTrait>;

impl Connector for Box<dyn AnyConnectorTrait> {
    type Transport = Box<dyn Transport>;
    fn connect(&self, details: &ConnectionDetails) -> Result<Self::Transport, Error> {
        (**self).connect(details)
    }
}

pub type AnyTransport = Box<dyn Transport>;

pub trait MiddleConnector<T: Transport>: Debug + Send + Sync + 'static {
    type Transport: Transport;

    fn connect(&self, details: &ConnectionDetails, chained: T) -> Result<Self::Transport, Error>;
}

With an implementation on tuples of many sizes through a macro:

impl<C, M1, M2> Connector for (C, M1, M2)
where
    C: Connector,
    M1: MiddleConnector<C::Transport>,
    M2: MiddleConnector<M1::Transport>,
{
    type Transport = M2::Transport;

    fn connect(&self, details: &ConnectionDetails) -> Result<Self::Transport, Error> {
        self.2
            .connect(details, self.1.connect(details, self.0.connect(details)?)?)
    }
}

You could still keep the ChainedConnector for one Connector + a chain of MiddleConnector<AnyTransport> that could also be boxed.

For connectors that may or may not "chain" like the SocksConnector followed by the TcpConnector, I would instead implement the SocksConnector as a:

struct SocksConnector<C: Connector<Transport = TcpStream>> {
    inner: C,
}

impl<C: Connector<Transport = TcpStream>> Connector for SocksConnector<C> { 
    type Transport = TcpStream;
    fn connect(&self, details: &ConnectionDetails) -> Result<Self::Transport, Error> {
        if !details.use_socks {
            return self.inner.connect(details);
        }
        // Current socks connector logic
    }
}

This type safety would allow for example a keepalive connector, which is much simpler that re-implementing all the functionality of the TcpConnector just to add that. It makes connectors much more composable.

struct KeepaliveConnector;

impl MiddleConnector<TcpTransport> for KeepaliveConnector {
    type Transport = TcpTransport;
    fn connect(
        &self,
        details: &ConnectionDetails,
        chained: TcpTransport,
    ) -> Result<Self::Transport, Error> {
        let stream: &std::net::TcpStream = chained.tcp_stream();
        // Configure Keepalive on the TcpStream;
        Ok(chained)
    }
}

The upstream connectors and transports are not public

This goes with the previous point. Building a custom chain of connectors could be simpler if the upstream connectors were public.
I understand that doing so would be a semver hazard, so maybe they should be in a separate crate (ureq-utils maybe?), to allow building on top of them, but allow the connectors to be more frequently updated.

The typestate variants are not public

The types WithoutBody and WithBody are not public (same for the Config scopes), which means it's not possible to write function that expect or return a RequestBuilder<WithoutBody>.

If you do not want these types to be public for semver reasons, would it be possible to still expose a type alias: type RequestWithBody = RequestBuilder<WithBody>,and type RequestWithoutBody = RequestBuilder<WithoutBody>, so that they be used in functions signatures?

Thank you for the work on 3.0.0, this looks like it will certainly help ureq be more flexible!

If you want I can create dedicated issues (or even PRs) for the parts you think are worth it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions