-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor library so that it can work with any HTTP client crate #97
Comments
Indeed it's a problem, and I've been facing this while I work on adding Tor support with Thanks for the suggestion! I've been working on this one, but with a different approach, here: 9cbc387 It mainly focuses on having a trait (e.g AFAICT it would require to anyone that's implementing a new client just to implement this trait for the client, as each client could (e.g reqwest) or not have (e.g hyper) useful methods for parsing, as long as they're parse to the correct types. I would say it's on a higher level, specifically for the methods+parsing we expect. It wouldn't require for us and users have to implement a trait for each method on esplora API 🤔. I'm wondering which one of both approaches would require minimal changes or be better on the long run. I'll tidy up my PR and also take a deeper look on your suggestion. |
I think it's okay to break things completely since it's just a client library. Aiming for "minimal changes" shouldn't be one of our requirements imo.
Correction: It's us (the devs of Abstracting out the transport (IO) means we need two traits, one for blocking and one for async. Abstracting out the request formation and response handling means we only need one trait (since this will not change regardless of the transport). Requiring users to impl a trait for an HTTP client type means they need to wrap the HTTP client type. Since one cannot impl an external trait for an external type. However, having each endpoint as a method on a type is a nicer API. It makes all endpoints easier to find than separate types for each endpoint. |
In this regard, I want to mention that we're indeed leaning on specific error codes and |
Cool! I've thinking a bit about both approaches. I will move and explore more towards your proposed one. As minimal changes are not a problem, it's better to make it generic over the request/response instead of IO. It becomes simple enough for the user, and more future-proof instead of the one I was working on. --
Good! Thanks for raising awareness on error handling, I'll have that in mind while working at it. |
I think there is no point in having multiple request methods on |
Why would anyone care about what HTTP client is being used under the hood? I mean as long as it does HTTP correctly. iirc the reason we had two different HTTP clients in the first place was that @tcharding needed something that compiled to SGX. reqwest didn't but ureq did. Then somehow we got minireq (can't remember why). I think we should just choose an HTTP client and stick with it. We should use the same for blocking and non-blocking or even consider removing the non-blocking version. |
I can think of a couple examples where this is useful.
I agree with this. However, making this library easily extensible would be useful imo. |
The better way to design this is to have the option for the user to pass in the TCP connection (or whatever) to the HTTP client: https://docs.rs/hyper/latest/hyper/client/conn/http1/fn.handshake.html
This seems severely under motivated. I'm pretty sure we are making a bunch of parallel connections anyway. Unless we are using HTTP 2.0 now? The question about whether HTTP 2.0 is important because if we are creating lots of parallel connections then we'd have to pass in a "connection builder" to do parallel requests. If it's HTTP 2.0 we can just reuse the same connection always.
Yes but not extensible with multiple HTTP implementations. There's one way to do it. Stick to one implementation. |
I do agree, but AFAICT
Still, we'd need to have a single implementation that does both blocking/non-blocking, right? However, we could also be opinionated about that (e.g supporting only blocking), as long as the user has alternatives. I think that by making it extensible, we can choose to support a single implementation (e.g. minreq), while still making it simple enough for the user who would like to use something different to implement a custom handler with its HTTP client of choice. |
I might be guilty but I don't remember sorry, it was almost 3 years ago I stopped messing with SGX. |
@LLFourn, I guess you are suggesting that we stick to I think this is a great long-term solution. However, there is a lot of features that We can figure how to provide these features out-of-the-box with For TOR HTTP support via
I think it's easier to run async code in a blocking code-base than it is to do the reverse. I would be okay to support async only, and get users to use something like In summary, I think for now, at a minimum, we should support both I think my proposal still makes sense for this. It's either we abstract the transport side of things, or we abstract out the data model. Working with |
ACK on it, I think it intersects with the ecosystem pain of not having an async HTTP client alternative to Also agree it would be a great addition not only for I also think it should be a different project/library that this one would rely on, and could lead to removing hyper and relying on it (as it's built on top of it).
I think the only problem with However, it's an issue we already have today, until we have a better async option for the ecosystem, as it were discussed about at the summit.
Yes, I think we can keep this approach, starting at #99, but only with two supported clients for now, Abstracting the data model would allow us to still have a |
Hmm, so I'm wondering: if everybody is aware of the issues Or to ask differently: what concrete/pressing issue is the complete refactoring of the |
The pain point is how to use a custom transport (not TCP) such as TOR. To do this with our current codebase means duplicating a lot of code. There will be no throwaway work. In the future, if there is a new HTTP client which allows us to swap out the transport, with all those amazing features built in, this proposal will allow us to support it with ease with no breaking changes. I think bugs will be to a minimum. This is a HTTP client library after all. As long as we test every endpoint, it is fine. |
Five hours to be exact! It took me much longer than I thought. I wanted to figure out whether what I was suggesting would work and so I just I did it: https://github.com/LLFourn/hyper-http-cli I implemented CONNECT proxy and ALPN. These were not really the hard bits. The hard bit is actually just figuring how how to glue these things together and all the circumstances where something would only work on one HTTP server but not others. There are several not obvious "util" crates you need to get things done. I'd say hyper + tokio + rustls is the way to go despite my experience being a bit hairy. At least hyper is mostly decoupled from TLS and tokio so either could be switched out later. It would be way better if we had HTTP and TLS implementations that were done sans-io but this is world we live in.
Yes I think we should only support async especially in this crate where that is mandatory due to needing to support WASM.
I don't see how this works. How are you going to use ALPN logic from reqwest with arti for example. The main benefit of reqwest is that it supports WASM transparently -- which is part of the reason it doesn't support BYO TCP/TLS connection. I guess that we will want reqwest if the target is wasm and just implement the client purely with reqwest with a bunch of stuff disabled if it's WASM.
I really think there is no benefit to using different HTTP impls. We should just choose the best one that:
As far as I can see that's hyper. I wouldn't really mind if we had a blocking client that satisfied these two things. We could manage two different clients like we do now. But if hyper is the answer then I'd say we just go async only and leave the fake "blocking" to the user. |
Just a side note, I think fake blocking is very easy to do. |
If we wanted to take flexibility to the next level we could implement the HTTP protocol sans-io on the bdk_esplora side like: pub struct EsploraSyncSession<I> {
sync_request: SyncRequest<I>
}
impl EsploraSyncSession {
pub fn poll_request(&mut self) -> Option<(RequestHandle, ResourceRequest)> {
// e.g. take stuff from internal `sync_request` and turn it into `ResourceRequest`
todo!()
}
pub fn provide_response(&mut self, handle: RequestHandle, Resource) -> Result<(), ResponseError> {
// insert stuff into internal SyncResult
todo!()
}
pub fn is_finished(&self) -> bool { todo!() }
pub fn into_update(self) -> TxUpdate<...> { todo!() }
}
struct ResourceRequest {
path: String // e.g. "/api/blocks/tip/height",
method: &'static str // e.g. GET, POST
body: Vec<u8> // mostly (maybe always?) empty
}
// leave up to imagination to fill in the other things Then we wouldn't even need this crate for syncing in |
The Problem
Right now
rust-esplora-client
is designed to only work with HTTP client crates that are baked in (i.e.minreq
for blocking,reqwest
for async). However, these HTTP client crates may not be preferred by the caller. The caller may also wish to use a different version of the HTTP client crate.Proposed Solution
Represent each endpoint call as a distinct type. Introduce a trait that is to be implemented for each endpoint call type. This trait will contain the template of how to form the HTTP request and parse the HTTP response body and status code.
Examples of endpoint types...
Example for calling any endpoint with
minreq
:Example for calling any endpoint with
reqwest
:In Conclusion
As can be seen, the code needed for a HTTP client crate to work with the trait is minimal.
The text was updated successfully, but these errors were encountered: