json_rpc/transports/http.rs
1//! HTTP-based transport for JSON-RPC 2.0.
2//!
3//! This module implements HTTP-based transport for JSON-RPC 2.0 communication
4//! using axum for the web server. It supports the standard JSON-RPC POST pattern
5//! where requests are sent via HTTP POST and responses are returned in the HTTP response.
6
7use std::sync::Arc;
8
9use axum::{
10 Router,
11 extract::{Request as AxumRequest, State},
12 http::{StatusCode, header},
13 response::{IntoResponse, Response},
14 routing::post,
15};
16
17use crate::error::Error;
18use crate::methods::Methods;
19use crate::transports::Transport;
20
21/// Default port for the HTTP server.
22const DEFAULT_PORT: u16 = 3000;
23
24/// Default path for JSON-RPC endpoints.
25const DEFAULT_PATH: &str = "/jsonrpc";
26
27/// Shared state for the HTTP server.
28#[derive(Clone)]
29struct HttpState {
30 /// The method registry for processing JSON-RPC requests.
31 methods: Arc<Methods>,
32}
33
34/// HTTP-based transport for JSON-RPC messages.
35///
36/// This transport uses axum to handle HTTP POST requests with JSON-RPC messages.
37/// Each HTTP POST request is treated as a JSON-RPC request, and the response
38/// is returned in the HTTP response body.
39///
40/// # Architecture
41///
42/// When a request arrives via HTTP POST:
43/// 1. The handler reads the request body as JSON
44/// 2. The JSON is processed through `methods.process_message()`
45/// 3. If a response is generated, it's returned with Content-Type: application/json
46/// 4. If no response is needed (notification), an empty 200 OK is returned
47///
48/// This design is much simpler than channel-based approaches because HTTP is
49/// inherently request/response - we don't need to manage pending responses
50/// or correlate requests with responses.
51///
52/// # Example
53///
54/// ```no_run
55/// use json_rpc::{Http, Methods};
56/// use serde_json::Value;
57/// use std::net::SocketAddr;
58///
59/// async fn echo(params: Value) -> Result<Value, json_rpc::Error> {
60/// Ok(params)
61/// }
62///
63/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
64/// let methods = Methods::new().add("echo", echo);
65/// let addr: SocketAddr = "127.0.0.1:3000".parse().unwrap();
66/// let transport = Http::new(addr);
67/// json_rpc::serve(transport, methods).await.unwrap();
68/// # });
69/// ```
70pub struct Http {
71 /// The address to bind the HTTP server to.
72 address: std::net::SocketAddr,
73}
74
75impl Http {
76 /// Create a new HTTP transport with the specified bind address.
77 ///
78 /// The server will accept POST requests at `/jsonrpc` on the specified address.
79 ///
80 /// # Example
81 ///
82 /// ```no_run
83 /// use json_rpc::Http;
84 /// use std::net::SocketAddr;
85 ///
86 /// // Default address
87 /// let addr: SocketAddr = "127.0.0.1:3000".parse().unwrap();
88 /// let transport = Http::new(addr);
89 ///
90 /// // Custom address
91 /// let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
92 /// let transport = Http::new(addr);
93 /// ```
94 pub fn new(address: std::net::SocketAddr) -> Self {
95 Self { address }
96 }
97}
98
99impl Transport for Http {
100 /// Serve the JSON-RPC server using HTTP transport.
101 ///
102 /// This method starts an axum HTTP server that accepts POST requests
103 /// at `/jsonrpc`. Each request is processed as a JSON-RPC message and
104 /// the response is returned in the HTTP response.
105 ///
106 /// The server runs until it is shut down (e.g., by Ctrl+C).
107 async fn serve(self, methods: Methods) -> Result<(), Error> {
108 let state = HttpState {
109 methods: Arc::new(methods),
110 };
111
112 let app = Router::new()
113 .route(DEFAULT_PATH, post(handle_jsonrpc))
114 .with_state(state);
115
116 let listener = tokio::net::TcpListener::bind(self.address)
117 .await
118 .map_err(|e| {
119 Error::TransportError(std::io::Error::new(
120 std::io::ErrorKind::AddrInUse,
121 format!("Failed to bind to port {}: {}", DEFAULT_PORT, e),
122 ))
123 })?;
124
125 let local_addr = listener.local_addr().map_err(Error::TransportError)?;
126
127 eprintln!("HTTP transport listening on http://{}", local_addr);
128 eprintln!("JSON-RPC endpoint: http://{}{}", local_addr, DEFAULT_PATH);
129
130 axum::serve(listener, app).await.map_err(|e| {
131 Error::TransportError(std::io::Error::other(format!("HTTP server error: {}", e)))
132 })?;
133
134 Ok(())
135 }
136}
137
138/// Handle HTTP POST requests for JSON-RPC messages.
139///
140/// This Axum handler extracts the JSON from the request body, processes it
141/// through the method registry, and returns the JSON-RPC response.
142async fn handle_jsonrpc(State(state): State<HttpState>, request: AxumRequest) -> Response {
143 let bytes = match axum::body::to_bytes(request.into_body(), 10 * 1024 * 1024).await {
144 Ok(b) => b,
145 Err(e) => {
146 return (
147 StatusCode::BAD_REQUEST,
148 format!("Failed to read body: {}", e),
149 )
150 .into_response();
151 }
152 };
153
154 let json_str = match String::from_utf8(bytes.to_vec()) {
155 Ok(s) => s,
156 Err(_) => {
157 return (StatusCode::BAD_REQUEST, "Invalid UTF-8 in request body").into_response();
158 }
159 };
160
161 let response_json = state.methods.process_message(&json_str).await;
162
163 match response_json {
164 Some(json) => (
165 StatusCode::OK,
166 [(header::CONTENT_TYPE, "application/json")],
167 json,
168 )
169 .into_response(),
170 None => StatusCode::OK.into_response(),
171 }
172}