Skip to content

Commit c26d49f

Browse files
avi-starkwareclaude
andcommitted
starknet_transaction_prover: add /health endpoint via HealthLayer
Adds a tower middleware that short-circuits `GET /health` ahead of jsonrpsee (which would 405 a GET) and returns `{"status":"ok"}`. Wired outermost in both the HTTP and HTTPS middleware chains so load balancers can probe before any per-request work runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c219d32 commit c26d49f

4 files changed

Lines changed: 143 additions & 1 deletion

File tree

crates/starknet_transaction_prover/src/server.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@ pub const OHTTP_JSONRPSEE_BODY_BUILDER: fn(Full<Bytes>) -> HttpBody = HttpBody::
2929
pub mod config;
3030
pub mod cors;
3131
pub mod errors;
32+
pub mod health;
3233
#[cfg(test)]
3334
pub mod mock_rpc;
3435
pub mod rpc_api;
3536
pub mod rpc_impl;
3637
pub mod tls;
3738

39+
pub use health::{HealthLayer, HEALTH_PATH};
40+
3841
#[cfg(test)]
3942
mod rpc_spec_test;
4043

@@ -75,7 +78,10 @@ pub async fn start_server(
7578
// type it expects. `HttpBody::new` is a zero-cost wrapper, so
7679
// non-OHTTP requests still stream through unbuffered.
7780
.set_http_middleware(
81+
// `HealthLayer` sits outermost so `GET /health` is answered
82+
// before any other middleware runs.
7883
ServiceBuilder::new()
84+
.layer(HealthLayer)
7985
.option_layer(cors_layer)
8086
.layer(MapRequestBodyLayer::new(HttpBody::new))
8187
.option_layer(ohttp_layer)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! HTTP `/health` endpoint as a tower middleware layer.
2+
//!
3+
//! Short-circuits `GET /health` before the jsonrpsee service sees the request
4+
//! (which would 405 a GET). Any other request passes through unchanged.
5+
6+
use std::task::{Context, Poll};
7+
8+
use bytes::Bytes;
9+
use futures::future::{ready, Either, Ready};
10+
use http::{header, Method, Request, Response, StatusCode};
11+
use http_body_util::Full;
12+
use jsonrpsee::server::HttpBody;
13+
use tower::{Layer, Service};
14+
15+
#[cfg(test)]
16+
#[path = "health_test.rs"]
17+
mod health_test;
18+
19+
pub const HEALTH_PATH: &str = "/health";
20+
21+
const HEALTHY_BODY: &[u8] = br#"{"status":"ok"}"#;
22+
23+
#[derive(Clone, Copy, Default)]
24+
pub struct HealthLayer;
25+
26+
impl<S> Layer<S> for HealthLayer {
27+
type Service = HealthService<S>;
28+
29+
fn layer(&self, inner: S) -> Self::Service {
30+
HealthService { inner }
31+
}
32+
}
33+
34+
#[derive(Clone)]
35+
pub struct HealthService<S> {
36+
inner: S,
37+
}
38+
39+
impl<S, ReqB> Service<Request<ReqB>> for HealthService<S>
40+
where
41+
S: Service<Request<ReqB>, Response = Response<HttpBody>>,
42+
{
43+
type Response = Response<HttpBody>;
44+
type Error = S::Error;
45+
type Future = Either<Ready<Result<Self::Response, Self::Error>>, S::Future>;
46+
47+
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
48+
// Always ready: the health fast-path doesn't need the inner service.
49+
// Inner backpressure is driven on demand by `inner.call` below.
50+
Poll::Ready(Ok(()))
51+
}
52+
53+
fn call(&mut self, request: Request<ReqB>) -> Self::Future {
54+
if request.method() == Method::GET && request.uri().path() == HEALTH_PATH {
55+
let response = Response::builder()
56+
.status(StatusCode::OK)
57+
.header(header::CONTENT_TYPE, "application/json")
58+
.body(HttpBody::new(Full::new(Bytes::from_static(HEALTHY_BODY))))
59+
.expect("response build with a static body is infallible");
60+
return Either::Left(ready(Ok(response)));
61+
}
62+
Either::Right(self.inner.call(request))
63+
}
64+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use bytes::Bytes;
2+
use http::{Method, Request, Response, StatusCode};
3+
use http_body_util::{BodyExt, Full};
4+
use jsonrpsee::server::HttpBody;
5+
use tower::{Layer, ServiceExt};
6+
7+
use crate::server::health::{HealthLayer, HEALTH_PATH};
8+
9+
/// Inner stub returning 418 so we can tell whether `HealthLayer` short-circuited.
10+
fn fallthrough_service() -> impl tower::Service<
11+
Request<HttpBody>,
12+
Response = Response<HttpBody>,
13+
Error = std::convert::Infallible,
14+
Future = futures::future::Ready<Result<Response<HttpBody>, std::convert::Infallible>>,
15+
> + Clone {
16+
tower::service_fn(|_req: Request<HttpBody>| {
17+
let response = Response::builder()
18+
.status(StatusCode::IM_A_TEAPOT)
19+
.body(HttpBody::new(Full::new(Bytes::from_static(b"fallthrough"))))
20+
.expect("static body is infallible");
21+
futures::future::ready(Ok::<_, std::convert::Infallible>(response))
22+
})
23+
}
24+
25+
fn empty_request(method: Method, path: &str) -> Request<HttpBody> {
26+
Request::builder()
27+
.method(method)
28+
.uri(path)
29+
.body(HttpBody::new(Full::new(Bytes::new())))
30+
.expect("static body is infallible")
31+
}
32+
33+
async fn read_body(response: Response<HttpBody>) -> (StatusCode, Vec<u8>, http::HeaderMap) {
34+
let (parts, body) = response.into_parts();
35+
let bytes = body.collect().await.expect("body collect").to_bytes().to_vec();
36+
(parts.status, bytes, parts.headers)
37+
}
38+
39+
#[tokio::test]
40+
async fn get_health_returns_200_with_json_body() {
41+
let svc = HealthLayer.layer(fallthrough_service());
42+
43+
let response = svc.oneshot(empty_request(Method::GET, HEALTH_PATH)).await.unwrap();
44+
45+
let (status, body, headers) = read_body(response).await;
46+
assert_eq!(status, StatusCode::OK);
47+
assert_eq!(body, br#"{"status":"ok"}"#);
48+
assert_eq!(headers.get(http::header::CONTENT_TYPE).unwrap(), "application/json");
49+
}
50+
51+
#[tokio::test]
52+
async fn non_get_health_falls_through() {
53+
let svc = HealthLayer.layer(fallthrough_service());
54+
55+
let response = svc.oneshot(empty_request(Method::POST, HEALTH_PATH)).await.unwrap();
56+
57+
let (status, _body, _) = read_body(response).await;
58+
assert_eq!(status, StatusCode::IM_A_TEAPOT);
59+
}
60+
61+
#[tokio::test]
62+
async fn get_other_path_falls_through() {
63+
let svc = HealthLayer.layer(fallthrough_service());
64+
65+
let response = svc.oneshot(empty_request(Method::GET, "/")).await.unwrap();
66+
67+
let (status, _body, _) = read_body(response).await;
68+
assert_eq!(status, StatusCode::IM_A_TEAPOT);
69+
}

crates/starknet_transaction_prover/src/server/tls.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use tower_http::map_request_body::MapRequestBodyLayer;
2727
use tower_http::map_response_body::MapResponseBodyLayer;
2828
use tracing::warn;
2929

30-
use super::OhttpJsonrpseeLayer;
30+
use crate::server::{HealthLayer, OhttpJsonrpseeLayer};
3131

3232
/// Maximum time allowed for a TLS handshake before the connection is dropped.
3333
const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
@@ -59,7 +59,10 @@ pub async fn start_tls_server(
5959
let svc_builder = ServerBuilder::default()
6060
.set_config(server_config)
6161
.set_http_middleware(
62+
// `HealthLayer` sits outermost so `GET /health` is answered before
63+
// any other middleware runs.
6264
ServiceBuilder::new()
65+
.layer(HealthLayer)
6366
.option_layer(cors_layer)
6467
.layer(MapRequestBodyLayer::new(HttpBody::new))
6568
.option_layer(ohttp_layer)

0 commit comments

Comments
 (0)