Skip to content
This repository was archived by the owner on Sep 4, 2024. It is now read-only.

Commit e651798

Browse files
authored
Merge pull request #20 from stevenroose/roundtripper
Make jsonrpc client-agnostic
2 parents 49d64af + cbd6876 commit e651798

File tree

4 files changed

+70
-66
lines changed

4 files changed

+70
-66
lines changed

Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ name = "jsonrpc"
1515
path = "src/lib.rs"
1616

1717
[dependencies]
18+
base64-compat = "1.0.0"
19+
http = "0.1"
20+
1821
serde = "1"
1922
serde_derive = "1"
2023
serde_json = "1"
21-
hyper = "0.10"
2224

src/client.rs

+60-51
Original file line numberDiff line numberDiff line change
@@ -18,41 +18,58 @@
1818
//! and parsing responses
1919
//!
2020
21+
use std::{error, io};
2122
use std::collections::HashMap;
22-
use std::io;
23-
use std::io::Read;
2423
use std::sync::{Arc, Mutex};
2524

26-
use hyper;
27-
use hyper::client::Client as HyperClient;
28-
use hyper::header::{Authorization, Basic, ContentType, Headers};
2925
use serde;
26+
use base64;
27+
use http;
3028
use serde_json;
3129

3230
use super::{Request, Response};
3331
use util::HashableValue;
3432
use error::Error;
3533

34+
/// An interface for an HTTP roundtripper that handles HTTP requests.
35+
pub trait HttpRoundTripper {
36+
/// The type of the http::Response body.
37+
type ResponseBody: io::Read;
38+
/// The type for errors generated by the roundtripper.
39+
type Err: error::Error;
40+
41+
/// Make an HTTP request. In practice only POST request will be made.
42+
fn request(
43+
&self,
44+
http::Request<&[u8]>,
45+
) -> Result<http::Response<Self::ResponseBody>, Self::Err>;
46+
}
47+
3648
/// A handle to a remote JSONRPC server
37-
pub struct Client {
49+
pub struct Client<R: HttpRoundTripper> {
3850
url: String,
3951
user: Option<String>,
4052
pass: Option<String>,
41-
client: HyperClient,
53+
roundtripper: R,
4254
nonce: Arc<Mutex<u64>>,
4355
}
4456

45-
impl Client {
57+
impl<Rt: HttpRoundTripper + 'static> Client<Rt> {
4658
/// Creates a new client
47-
pub fn new(url: String, user: Option<String>, pass: Option<String>) -> Client {
59+
pub fn new(
60+
roundtripper: Rt,
61+
url: String,
62+
user: Option<String>,
63+
pass: Option<String>,
64+
) -> Client<Rt> {
4865
// Check that if we have a password, we have a username; other way around is ok
4966
debug_assert!(pass.is_none() || user.is_some());
5067

5168
Client {
5269
url: url,
5370
user: user,
5471
pass: pass,
55-
client: HyperClient::new(),
72+
roundtripper: roundtripper,
5673
nonce: Arc::new(Mutex::new(0)),
5774
}
5875
}
@@ -78,52 +95,30 @@ impl Client {
7895
// Build request
7996
let request_raw = serde_json::to_vec(body)?;
8097

81-
// Setup connection
82-
let mut headers = Headers::new();
83-
headers.set(ContentType::json());
98+
// Send request
99+
let mut request_builder = http::Request::post(&self.url);
100+
request_builder.header("Content-Type", "application/json-rpc");
101+
102+
// Set Authorization header
84103
if let Some(ref user) = self.user {
85-
headers.set(Authorization(Basic {
86-
username: user.clone(),
87-
password: self.pass.clone(),
88-
}));
104+
let mut auth = user.clone();
105+
auth.push(':');
106+
if let Some(ref pass) = self.pass {
107+
auth.push_str(&pass[..]);
108+
}
109+
let value = format!("Basic {}", &base64::encode(auth.as_bytes()));
110+
request_builder.header("Authorization", value);
89111
}
90112

91-
// Send request
92-
let retry_headers = headers.clone();
93-
let hyper_request = self.client.post(&self.url).headers(headers).body(&request_raw[..]);
94-
let mut stream = match hyper_request.send() {
95-
Ok(s) => s,
96-
// Hyper maintains a pool of TCP connections to its various clients,
97-
// and when one drops it cannot tell until it tries sending. In this
98-
// case the appropriate thing is to re-send, which will cause hyper
99-
// to open a new connection. Jonathan Reem explained this to me on
100-
// IRC, citing vague technical reasons that the library itself cannot
101-
// do the retry transparently.
102-
Err(hyper::error::Error::Io(e)) => {
103-
if e.kind() == io::ErrorKind::BrokenPipe
104-
|| e.kind() == io::ErrorKind::ConnectionAborted
105-
{
106-
try!(self
107-
.client
108-
.post(&self.url)
109-
.headers(retry_headers)
110-
.body(&request_raw[..])
111-
.send()
112-
.map_err(Error::Hyper))
113-
} else {
114-
return Err(Error::Hyper(hyper::error::Error::Io(e)));
115-
}
116-
}
117-
Err(e) => {
118-
return Err(Error::Hyper(e));
119-
}
120-
};
113+
// Errors only on invalid header or builder reuse.
114+
let http_request = request_builder.body(&request_raw[..]).unwrap();
115+
116+
let http_response =
117+
self.roundtripper.request(http_request).map_err(|e| Error::Http(Box::new(e)))?;
121118

122119
// nb we ignore stream.status since we expect the body
123120
// to contain information about any error
124-
let response: R = serde_json::from_reader(&mut stream)?;
125-
stream.bytes().count(); // Drain the stream so it can be reused
126-
Ok(response)
121+
Ok(serde_json::from_reader(http_response.into_body())?)
127122
}
128123

129124
/// Sends a request to a client
@@ -204,10 +199,24 @@ impl Client {
204199
#[cfg(test)]
205200
mod tests {
206201
use super::*;
202+
use std::io;
203+
204+
struct RT();
205+
impl HttpRoundTripper for RT {
206+
type ResponseBody = io::Empty;
207+
type Err = io::Error;
208+
209+
fn request(
210+
&self,
211+
_: http::Request<&[u8]>,
212+
) -> Result<http::Response<Self::ResponseBody>, Self::Err> {
213+
Err(io::ErrorKind::Other.into())
214+
}
215+
}
207216

208217
#[test]
209218
fn sanity() {
210-
let client = Client::new("localhost".to_owned(), None, None);
219+
let client = Client::new(RT(), "localhost".to_owned(), None, None);
211220
assert_eq!(client.last_nonce(), 0);
212221
let req1 = client.build_request("test", &[]);
213222
assert_eq!(client.last_nonce(), 1);

src/error.rs

+5-12
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
2020
use std::{error, fmt};
2121

22-
use hyper;
2322
use serde_json;
2423

2524
use Response;
@@ -29,8 +28,8 @@ use Response;
2928
pub enum Error {
3029
/// Json error
3130
Json(serde_json::Error),
32-
/// Client error
33-
Hyper(hyper::error::Error),
31+
/// HTTP client error
32+
Http(Box<error::Error>),
3433
/// Error response
3534
Rpc(RpcError),
3635
/// Response to a request did not have the expected nonce
@@ -53,12 +52,6 @@ impl From<serde_json::Error> for Error {
5352
}
5453
}
5554

56-
impl From<hyper::error::Error> for Error {
57-
fn from(e: hyper::error::Error) -> Error {
58-
Error::Hyper(e)
59-
}
60-
}
61-
6255
impl From<RpcError> for Error {
6356
fn from(e: RpcError) -> Error {
6457
Error::Rpc(e)
@@ -69,7 +62,7 @@ impl fmt::Display for Error {
6962
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
7063
match *self {
7164
Error::Json(ref e) => write!(f, "JSON decode error: {}", e),
72-
Error::Hyper(ref e) => write!(f, "Hyper error: {}", e),
65+
Error::Http(ref e) => write!(f, "HTTP error: {}", e),
7366
Error::Rpc(ref r) => write!(f, "RPC error response: {:?}", r),
7467
Error::BatchDuplicateResponseId(ref v) => {
7568
write!(f, "duplicate RPC batch response ID: {}", v)
@@ -84,7 +77,7 @@ impl error::Error for Error {
8477
fn description(&self) -> &str {
8578
match *self {
8679
Error::Json(_) => "JSON decode error",
87-
Error::Hyper(_) => "Hyper error",
80+
Error::Http(_) => "HTTP error",
8881
Error::Rpc(_) => "RPC error response",
8982
Error::NonceMismatch => "Nonce of response did not match nonce of request",
9083
Error::VersionMismatch => "`jsonrpc` field set to non-\"2.0\"",
@@ -100,7 +93,7 @@ impl error::Error for Error {
10093
fn cause(&self) -> Option<&error::Error> {
10194
match *self {
10295
Error::Json(ref e) => Some(e),
103-
Error::Hyper(ref e) => Some(e),
96+
Error::Http(ref e) => Some(&**e),
10497
_ => None,
10598
}
10699
}

src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
#![deny(unused_mut)]
2929
#![warn(missing_docs)]
3030

31-
extern crate hyper;
32-
31+
extern crate base64;
32+
extern crate http;
3333
extern crate serde;
3434
#[macro_use]
3535
extern crate serde_derive;

0 commit comments

Comments
 (0)