Skip to main content

lightning_block_sync/
http.rs

1//! Simple HTTP implementation which supports both async and traditional execution environments
2//! with minimal dependencies. This is used as the basis for REST and RPC clients.
3
4use serde_json;
5
6#[cfg(feature = "tokio")]
7use bitreq::RequestExt;
8
9use std::convert::Infallible;
10use std::convert::TryFrom;
11use std::fmt;
12
13/// Trait for converting parse errors into a String message.
14pub trait ToParseErrorMessage {
15	/// Converts a parse error into a human-readable message.
16	fn to_parse_error_message(self) -> String;
17}
18
19impl ToParseErrorMessage for Infallible {
20	fn to_parse_error_message(self) -> String {
21		match self {}
22	}
23}
24
25impl ToParseErrorMessage for () {
26	fn to_parse_error_message(self) -> String {
27		"invalid data".to_string()
28	}
29}
30
31impl ToParseErrorMessage for &'static str {
32	fn to_parse_error_message(self) -> String {
33		self.to_string()
34	}
35}
36
37impl ToParseErrorMessage for String {
38	fn to_parse_error_message(self) -> String {
39		self
40	}
41}
42
43/// Timeout for requests in seconds. This is set to a high value as it is not uncommon for Bitcoin
44/// Core to be blocked waiting on UTXO cache flushes for upwards of 10 minutes on slow devices
45/// (e.g. RPis with SSDs over USB).
46const TCP_STREAM_RESPONSE_TIMEOUT: u64 = 300;
47
48/// Maximum HTTP message body size in bytes. Enough for a hex-encoded block in JSON format and any
49/// overhead for HTTP chunked transfer encoding.
50const MAX_HTTP_MESSAGE_BODY_SIZE: usize = 2 * 4_000_000 + 32_000;
51
52/// Error type for HTTP client operations.
53#[derive(Debug)]
54pub enum HttpClientError {
55	/// transport-level error (connection, timeout, protocol parsing, etc.)
56	Transport(bitreq::Error),
57	/// HTTP error response (non-2xx status code)
58	Http(HttpError),
59	/// Response parsing/conversion error
60	Parse(String),
61}
62
63impl std::error::Error for HttpClientError {
64	fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
65		match self {
66			HttpClientError::Transport(e) => Some(e),
67			HttpClientError::Http(e) => Some(e),
68			HttpClientError::Parse(_) => None,
69		}
70	}
71}
72
73impl fmt::Display for HttpClientError {
74	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75		match self {
76			HttpClientError::Transport(e) => write!(f, "transport error: {}", e),
77			HttpClientError::Http(e) => write!(f, "HTTP error: {}", e),
78			HttpClientError::Parse(e) => write!(f, "response parsing error: {}", e),
79		}
80	}
81}
82
83impl From<bitreq::Error> for HttpClientError {
84	fn from(e: bitreq::Error) -> Self {
85		HttpClientError::Transport(e)
86	}
87}
88
89impl From<HttpError> for HttpClientError {
90	fn from(e: HttpError) -> Self {
91		HttpClientError::Http(e)
92	}
93}
94
95/// Maximum number of cached connections in the connection pool.
96#[cfg(feature = "tokio")]
97const MAX_CONNECTIONS: usize = 10;
98
99/// Client for making HTTP requests.
100pub(crate) struct HttpClient {
101	base_url: String,
102	#[cfg(feature = "tokio")]
103	client: bitreq::Client,
104}
105
106impl HttpClient {
107	/// Creates a new HTTP client for the given base URL.
108	///
109	/// The base URL should include the scheme, host, and port (e.g., "http://127.0.0.1:8332").
110	/// DNS resolution is deferred until the first request is made.
111	pub fn new(base_url: String) -> Self {
112		Self {
113			base_url,
114			#[cfg(feature = "tokio")]
115			client: bitreq::Client::new(MAX_CONNECTIONS),
116		}
117	}
118
119	/// Sends a `GET` request for a resource identified by `uri`.
120	///
121	/// Returns the response body in `F` format.
122	#[allow(dead_code)]
123	pub async fn get<F>(&self, uri: &str) -> Result<F, HttpClientError>
124	where
125		F: TryFrom<Vec<u8>>,
126		<F as TryFrom<Vec<u8>>>::Error: ToParseErrorMessage,
127	{
128		let url = format!("{}{}", self.base_url, uri);
129		let request = bitreq::get(url)
130			.with_timeout(TCP_STREAM_RESPONSE_TIMEOUT)
131			.with_max_body_size(Some(MAX_HTTP_MESSAGE_BODY_SIZE));
132		#[cfg(feature = "tokio")]
133		let request = request.with_pipelining();
134		let response_body = self.send_request(request).await?;
135		F::try_from(response_body).map_err(|e| HttpClientError::Parse(e.to_parse_error_message()))
136	}
137
138	/// Sends a `POST` request for a resource identified by `uri` using the given HTTP
139	/// authentication credentials.
140	///
141	/// The request body consists of the provided JSON `content`. Returns the response body in `F`
142	/// format.
143	#[allow(dead_code)]
144	pub async fn post<F>(
145		&self, uri: &str, auth: &str, content: serde_json::Value,
146	) -> Result<F, HttpClientError>
147	where
148		F: TryFrom<Vec<u8>>,
149		<F as TryFrom<Vec<u8>>>::Error: ToParseErrorMessage,
150	{
151		let url = format!("{}{}", self.base_url, uri);
152		let request = bitreq::post(url)
153			.with_header("Authorization", auth)
154			.with_header("Content-Type", "application/json")
155			.with_timeout(TCP_STREAM_RESPONSE_TIMEOUT)
156			.with_max_body_size(Some(MAX_HTTP_MESSAGE_BODY_SIZE))
157			.with_body(content.to_string());
158		#[cfg(feature = "tokio")]
159		let request = request.with_pipelining();
160		let response_body = self.send_request(request).await?;
161		F::try_from(response_body).map_err(|e| HttpClientError::Parse(e.to_parse_error_message()))
162	}
163
164	/// Sends an HTTP request message and reads the response, returning its body.
165	async fn send_request(&self, request: bitreq::Request) -> Result<Vec<u8>, HttpClientError> {
166		#[cfg(feature = "tokio")]
167		let response = request.send_async_with_client(&self.client).await?;
168		#[cfg(not(feature = "tokio"))]
169		let response = request.send()?;
170
171		let status_code = response.status_code;
172		let body = response.into_bytes();
173
174		if !(200..300).contains(&status_code) {
175			return Err(HttpError { status_code, contents: body }.into());
176		}
177
178		Ok(body)
179	}
180}
181
182/// HTTP error consisting of a status code and body contents.
183#[derive(Debug)]
184pub struct HttpError {
185	/// The HTTP status code.
186	pub status_code: i32,
187	/// The response body contents.
188	pub contents: Vec<u8>,
189}
190
191impl std::error::Error for HttpError {}
192
193impl fmt::Display for HttpError {
194	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
195		let contents = String::from_utf8_lossy(&self.contents);
196		write!(f, "status_code: {}, contents: {}", self.status_code, contents)
197	}
198}
199
200/// An HTTP response body in binary format.
201pub struct BinaryResponse(pub Vec<u8>);
202
203/// An HTTP response body in JSON format.
204pub struct JsonResponse(pub serde_json::Value);
205
206/// Interprets bytes from an HTTP response body as binary data.
207impl TryFrom<Vec<u8>> for BinaryResponse {
208	type Error = Infallible;
209
210	fn try_from(bytes: Vec<u8>) -> Result<Self, Infallible> {
211		Ok(BinaryResponse(bytes))
212	}
213}
214
215/// Interprets bytes from an HTTP response body as a JSON value.
216impl TryFrom<Vec<u8>> for JsonResponse {
217	type Error = String;
218
219	fn try_from(bytes: Vec<u8>) -> Result<Self, String> {
220		serde_json::from_slice(&bytes).map(JsonResponse).map_err(|e| e.to_string())
221	}
222}
223
224#[cfg(test)]
225pub(crate) mod client_tests {
226	use super::*;
227	use std::io::{BufRead, Read, Write};
228	use std::time::Duration;
229
230	/// Server for handling HTTP client requests with a stock response.
231	pub struct HttpServer {
232		address: std::net::SocketAddr,
233		handler: Option<std::thread::JoinHandle<()>>,
234		shutdown: std::sync::Arc<std::sync::atomic::AtomicBool>,
235	}
236
237	impl Drop for HttpServer {
238		fn drop(&mut self) {
239			self.shutdown.store(true, std::sync::atomic::Ordering::SeqCst);
240			// Make a connection to unblock the listener's accept() call
241			let _ = std::net::TcpStream::connect(self.address);
242			if let Some(handler) = self.handler.take() {
243				let _ = handler.join();
244			}
245		}
246	}
247
248	/// Body of HTTP response messages.
249	pub enum MessageBody<T: ToString> {
250		Empty,
251		Content(T),
252	}
253
254	impl HttpServer {
255		fn responding_with_body<T: ToString>(status: &str, body: MessageBody<T>) -> Self {
256			let response = match body {
257				MessageBody::Empty => format!(
258					"{}\r\n\
259					 Content-Length: 0\r\n\
260					 \r\n",
261					status
262				),
263				MessageBody::Content(body) => {
264					let body = body.to_string();
265					format!(
266						"{}\r\n\
267						 Content-Length: {}\r\n\
268						 \r\n\
269						 {}",
270						status,
271						body.len(),
272						body
273					)
274				},
275			};
276			HttpServer::responding_with(response)
277		}
278
279		pub fn responding_with_ok<T: ToString>(body: MessageBody<T>) -> Self {
280			HttpServer::responding_with_body("HTTP/1.1 200 OK", body)
281		}
282
283		pub fn responding_with_not_found() -> Self {
284			HttpServer::responding_with_body::<String>("HTTP/1.1 404 Not Found", MessageBody::Empty)
285		}
286
287		pub fn responding_with_server_error<T: ToString>(content: T) -> Self {
288			let body = MessageBody::Content(content);
289			HttpServer::responding_with_body("HTTP/1.1 500 Internal Server Error", body)
290		}
291
292		fn responding_with(response: String) -> Self {
293			let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
294			let address = listener.local_addr().unwrap();
295
296			let shutdown = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
297			let shutdown_signaled = std::sync::Arc::clone(&shutdown);
298			let handler = std::thread::spawn(move || {
299				let timeout = Duration::from_secs(5);
300				for stream in listener.incoming() {
301					if shutdown_signaled.load(std::sync::atomic::Ordering::SeqCst) {
302						return;
303					}
304
305					let stream = stream.unwrap();
306					stream.set_write_timeout(Some(timeout)).unwrap();
307					stream.set_read_timeout(Some(timeout)).unwrap();
308
309					let mut reader = std::io::BufReader::new(stream);
310
311					// Handle multiple requests on the same connection (keep-alive)
312					loop {
313						if shutdown_signaled.load(std::sync::atomic::Ordering::SeqCst) {
314							return;
315						}
316
317						// Read request headers
318						let mut lines_read = 0;
319						let mut content_length: usize = 0;
320						loop {
321							let mut line = String::new();
322							match reader.read_line(&mut line) {
323								Ok(0) => break, // eof
324								Ok(_) => {
325									if line == "\r\n" || line == "\n" {
326										break; // end of headers
327									}
328									// Parse content_length for POST body handling
329									if let Some(value) = line.strip_prefix("Content-Length:") {
330										content_length = value.trim().parse().unwrap_or(0);
331									}
332									lines_read += 1;
333								},
334								Err(_) => break, // Read error or timeout
335							}
336						}
337
338						if lines_read == 0 {
339							break; // No request received, connection closed
340						}
341
342						// Consume request body if present (needed for POST keep-alive)
343						if content_length > 0 {
344							let mut body = vec![0u8; content_length];
345							if reader.read_exact(&mut body).is_err() {
346								break;
347							}
348						}
349
350						// Send response
351						let stream = reader.get_mut();
352						let mut write_error = false;
353						for chunk in response.as_bytes().chunks(16) {
354							if shutdown_signaled.load(std::sync::atomic::Ordering::SeqCst) {
355								return;
356							}
357							if stream.write(chunk).is_err() || stream.flush().is_err() {
358								write_error = true;
359								break;
360							}
361						}
362						if write_error {
363							break;
364						}
365					}
366				}
367			});
368
369			Self { address, handler: Some(handler), shutdown }
370		}
371
372		pub fn endpoint(&self) -> String {
373			format!("http://{}:{}", self.address.ip(), self.address.port())
374		}
375	}
376
377	#[tokio::test]
378	async fn connect_with_invalid_host() {
379		let client = HttpClient::new("http://invalid.host.example:80".to_string());
380		match client.get::<JsonResponse>("/foo").await {
381			Err(HttpClientError::Transport(_)) => {},
382			Err(e) => panic!("Unexpected error type: {:?}", e),
383			Ok(_) => panic!("Expected error"),
384		}
385	}
386
387	#[tokio::test]
388	async fn read_error() {
389		let server = HttpServer::responding_with_server_error("foo");
390
391		let client = HttpClient::new(server.endpoint());
392		match client.get::<JsonResponse>("/foo").await {
393			Err(HttpClientError::Http(http_error)) => {
394				assert_eq!(http_error.status_code, 500);
395				assert_eq!(http_error.contents, "foo".as_bytes());
396			},
397			Err(e) => panic!("Unexpected error type: {:?}", e),
398			Ok(_) => panic!("Expected error"),
399		}
400	}
401
402	#[tokio::test]
403	async fn read_message_body() {
404		let body = "foo bar baz qux".repeat(32);
405		let content = MessageBody::Content(body.clone());
406		let server = HttpServer::responding_with_ok::<String>(content);
407
408		let client = HttpClient::new(server.endpoint());
409		match client.get::<BinaryResponse>("/foo").await {
410			Err(e) => panic!("Unexpected error: {:?}", e),
411			Ok(bytes) => assert_eq!(bytes.0, body.as_bytes()),
412		}
413	}
414
415	#[tokio::test]
416	async fn reconnect_closed_connection() {
417		let server = HttpServer::responding_with_ok::<String>(MessageBody::Empty);
418
419		let client = HttpClient::new(server.endpoint());
420		assert!(client.get::<BinaryResponse>("/foo").await.is_ok());
421		match client.get::<BinaryResponse>("/foo").await {
422			Err(e) => panic!("Unexpected error: {:?}", e),
423			Ok(bytes) => assert_eq!(bytes.0, Vec::<u8>::new()),
424		}
425	}
426
427	#[test]
428	fn from_bytes_into_binary_response() {
429		let bytes = b"foo";
430		match BinaryResponse::try_from(bytes.to_vec()) {
431			Err(e) => panic!("Unexpected error: {:?}", e),
432			Ok(response) => assert_eq!(&response.0, bytes),
433		}
434	}
435
436	#[test]
437	fn from_invalid_bytes_into_json_response() {
438		let json = serde_json::json!({ "result": 42 });
439		match JsonResponse::try_from(json.to_string().as_bytes()[..5].to_vec()) {
440			Err(_) => {},
441			Ok(_) => panic!("Expected error"),
442		}
443	}
444
445	#[test]
446	fn from_valid_bytes_into_json_response() {
447		let json = serde_json::json!({ "result": 42 });
448		match JsonResponse::try_from(json.to_string().as_bytes().to_vec()) {
449			Err(e) => panic!("Unexpected error: {:?}", e),
450			Ok(response) => assert_eq!(response.0, json),
451		}
452	}
453}