Skip to main content

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}