review(http): mark http/review-mcp completed + fix formatting across crate
Review-mcp verification complete: all 12 checklist items pass (from_mcp/to_mcp conformance, ADR-037/041/014/023/034, feature gate isolation, GatewayDispatch concrete struct, test coverage 223+5). Applied cargo fmt across crate.
This commit is contained in:
1 parent
48ead6950b
commit
58e16d088b
17 files changed
+538
-337
No files matched your search
@@ -33,12 +33,12 @@ use super::auth::bearer_auth_middleware;
|
||||
use super::decoy::decoy_fallback;
|
||||
use super::gateway_routes;
|
||||
use super::healthz::healthz;
|
||||
use crate::websocket::upgrade::ws_upgrade_handler;
|
||||
use crate::websocket::upgrade::WS_UPGRADE_PATH;
|
||||
#[cfg(feature = "mcp")]
|
||||
use crate::adapters::to_mcp_service;
|
||||
#[cfg(feature = "mcp")]
|
||||
use crate::gateway::GatewayDispatch;
|
||||
use crate::websocket::upgrade::ws_upgrade_handler;
|
||||
use crate::websocket::upgrade::WS_UPGRADE_PATH;
|
||||
|
||||
const ALPN_HTTP1: &[u8] = b"http/1.1";
|
||||
const ALPN_H2: &[u8] = b"h2";
|
||||
@@ -47,8 +47,12 @@ const ALPN_H2: &[u8] = b"h2";
|
||||
pub enum DecoyConfig {
|
||||
#[default]
|
||||
NotFound,
|
||||
StaticSite { root: PathBuf },
|
||||
Redirect { to: String },
|
||||
StaticSite {
|
||||
root: PathBuf,
|
||||
},
|
||||
Redirect {
|
||||
to: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -87,11 +91,17 @@ pub struct HttpAdapter {
|
||||
}
|
||||
|
||||
impl HttpAdapter {
|
||||
pub fn new(identity_provider: Arc<dyn IdentityProvider>, registry: Arc<OperationRegistry>) -> Self {
|
||||
pub fn new(
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
registry: Arc<OperationRegistry>,
|
||||
) -> Self {
|
||||
Self::for_alpn(identity_provider, registry, ALPN_HTTP1)
|
||||
}
|
||||
|
||||
pub fn h2(identity_provider: Arc<dyn IdentityProvider>, registry: Arc<OperationRegistry>) -> Self {
|
||||
pub fn h2(
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
registry: Arc<OperationRegistry>,
|
||||
) -> Self {
|
||||
Self::for_alpn(identity_provider, registry, ALPN_H2)
|
||||
}
|
||||
|
||||
@@ -163,7 +173,10 @@ fn build_router(state: RouterState, extra_routes: Option<Router>) -> Router {
|
||||
));
|
||||
Router::new()
|
||||
.nest_service("/mcp", to_mcp_service(dispatch))
|
||||
.layer(from_fn_with_state(auth_state.clone(), bearer_auth_middleware))
|
||||
.layer(from_fn_with_state(
|
||||
auth_state.clone(),
|
||||
bearer_auth_middleware,
|
||||
))
|
||||
};
|
||||
#[cfg(not(feature = "mcp"))]
|
||||
let mcp_router: Router<RouterState> = Router::new();
|
||||
@@ -172,7 +185,10 @@ fn build_router(state: RouterState, extra_routes: Option<Router>) -> Router {
|
||||
.merge(gateway_routes::gateway_router())
|
||||
.route("/openapi.json", get(not_implemented))
|
||||
.route(WS_UPGRADE_PATH, get(ws_upgrade_handler))
|
||||
.route_layer(from_fn_with_state(auth_state.clone(), bearer_auth_middleware))
|
||||
.route_layer(from_fn_with_state(
|
||||
auth_state.clone(),
|
||||
bearer_auth_middleware,
|
||||
))
|
||||
.route("/healthz", get(healthz))
|
||||
.fallback(decoy_fallback)
|
||||
.merge(mcp_router);
|
||||
@@ -203,7 +219,10 @@ impl ProtocolHandler for HttpAdapter {
|
||||
let _ = connection.set_identity(identity);
|
||||
}
|
||||
|
||||
let (send, recv) = connection.accept_bi().await.map_err(stream_error_to_handler)?;
|
||||
let (send, recv) = connection
|
||||
.accept_bi()
|
||||
.await
|
||||
.map_err(stream_error_to_handler)?;
|
||||
let io = QuicStream::new(send, recv);
|
||||
self.serve_io(io).await
|
||||
}
|
||||
@@ -295,7 +314,10 @@ mod tests {
|
||||
fn resolve_from_fingerprint(&self, _: &str) -> Option<alknet_core::auth::Identity> {
|
||||
None
|
||||
}
|
||||
fn resolve_from_token(&self, _: &alknet_core::auth::AuthToken) -> Option<alknet_core::auth::Identity> {
|
||||
fn resolve_from_token(
|
||||
&self,
|
||||
_: &alknet_core::auth::AuthToken,
|
||||
) -> Option<alknet_core::auth::Identity> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -341,7 +363,9 @@ mod tests {
|
||||
#[test]
|
||||
fn with_decoy_updates_decoy() {
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry());
|
||||
let adapter = adapter.with_decoy(DecoyConfig::Redirect { to: "https://example.com".to_string() });
|
||||
let adapter = adapter.with_decoy(DecoyConfig::Redirect {
|
||||
to: "https://example.com".to_string(),
|
||||
});
|
||||
assert!(matches!(adapter.decoy(), DecoyConfig::Redirect { .. }));
|
||||
}
|
||||
|
||||
@@ -386,7 +410,10 @@ mod tests {
|
||||
) -> (String, tokio::task::JoinHandle<()>) {
|
||||
let (mut client_send, server_recv) = duplex(8 * 1024);
|
||||
let (server_send, mut client_recv) = duplex(8 * 1024);
|
||||
let server_io = QuicStreamDuplex { read: server_recv, write: server_send };
|
||||
let server_io = QuicStreamDuplex {
|
||||
read: server_recv,
|
||||
write: server_send,
|
||||
};
|
||||
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry());
|
||||
let handle = tokio::spawn(async move {
|
||||
@@ -399,7 +426,12 @@ mod tests {
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), client_recv.read(&mut buf)).await {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client_recv.read(&mut buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(0)) => break,
|
||||
Ok(Ok(n)) => response.extend_from_slice(&buf[..n]),
|
||||
Ok(Err(_)) => break,
|
||||
@@ -455,21 +487,24 @@ mod tests {
|
||||
let request = b"GET /healthz HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
let (response, handle) = send_request_and_read_response(request).await;
|
||||
handle.await.ok();
|
||||
assert!(response.starts_with("HTTP/1.1 200 "), "expected 200, got: {response}");
|
||||
assert!(
|
||||
response.starts_with("HTTP/1.1 200 "),
|
||||
"expected 200, got: {response}"
|
||||
);
|
||||
assert!(response.contains("\r\n\r\nok"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn custom_route_v1_foo_coexists_with_default_surface() {
|
||||
let extra = Router::new().route(
|
||||
"/v1/foo",
|
||||
get(|| async { (StatusCode::OK, "foo-body") }),
|
||||
);
|
||||
let extra = Router::new().route("/v1/foo", get(|| async { (StatusCode::OK, "foo-body") }));
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry()).with_extra_routes(extra);
|
||||
|
||||
let (mut client_send, server_recv) = duplex(8 * 1024);
|
||||
let (server_send, mut client_recv) = duplex(8 * 1024);
|
||||
let server_io = QuicStreamDuplex { read: server_recv, write: server_send };
|
||||
let server_io = QuicStreamDuplex {
|
||||
read: server_recv,
|
||||
write: server_send,
|
||||
};
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
adapter.serve_io(server_io).await.ok();
|
||||
@@ -482,7 +517,12 @@ mod tests {
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), client_recv.read(&mut buf)).await {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client_recv.read(&mut buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(0)) => break,
|
||||
Ok(Ok(n)) => response.extend_from_slice(&buf[..n]),
|
||||
Ok(Err(_)) => break,
|
||||
@@ -491,7 +531,10 @@ mod tests {
|
||||
}
|
||||
handle.await.ok();
|
||||
let response_str = String::from_utf8_lossy(&response);
|
||||
assert!(response_str.starts_with("HTTP/1.1 200 "), "expected 200, got: {response_str}");
|
||||
assert!(
|
||||
response_str.starts_with("HTTP/1.1 200 "),
|
||||
"expected 200, got: {response_str}"
|
||||
);
|
||||
assert!(response_str.contains("foo-body"));
|
||||
}
|
||||
|
||||
@@ -505,7 +548,10 @@ mod tests {
|
||||
|
||||
let (mut client_send, server_recv) = duplex(8 * 1024);
|
||||
let (server_send, mut client_recv) = duplex(8 * 1024);
|
||||
let server_io = QuicStreamDuplex { read: server_recv, write: server_send };
|
||||
let server_io = QuicStreamDuplex {
|
||||
read: server_recv,
|
||||
write: server_send,
|
||||
};
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
adapter.serve_io(server_io).await.ok();
|
||||
@@ -518,7 +564,12 @@ mod tests {
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), client_recv.read(&mut buf)).await {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client_recv.read(&mut buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(0)) => break,
|
||||
Ok(Ok(n)) => response.extend_from_slice(&buf[..n]),
|
||||
Ok(Err(_)) => break,
|
||||
@@ -527,7 +578,10 @@ mod tests {
|
||||
}
|
||||
handle.await.ok();
|
||||
let response_str = String::from_utf8_lossy(&response);
|
||||
assert!(response_str.starts_with("HTTP/1.1 200 "), "default GET /healthz wins, got: {response_str}");
|
||||
assert!(
|
||||
response_str.starts_with("HTTP/1.1 200 "),
|
||||
"default GET /healthz wins, got: {response_str}"
|
||||
);
|
||||
assert!(response_str.contains("\r\n\r\nok"));
|
||||
assert!(!response_str.contains("custom-healthz"));
|
||||
}
|
||||
@@ -547,7 +601,12 @@ mod tests {
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), client_recv.read(&mut buf)).await {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client_recv.read(&mut buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(0)) => break,
|
||||
Ok(Ok(n)) => response.extend_from_slice(&buf[..n]),
|
||||
Ok(Err(_)) => break,
|
||||
@@ -569,7 +628,10 @@ mod tests {
|
||||
.with_extra_routes(extra);
|
||||
let request = b"POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: 0\r\n\r\n";
|
||||
let response = serve_and_read(adapter, request).await;
|
||||
assert!(response.starts_with("HTTP/1.1 200"), "expected 200, got: {response}");
|
||||
assert!(
|
||||
response.starts_with("HTTP/1.1 200"),
|
||||
"expected 200, got: {response}"
|
||||
);
|
||||
assert!(response.contains("oai-proxy"));
|
||||
assert!(!response.contains("404 Not Found"));
|
||||
}
|
||||
@@ -583,32 +645,43 @@ mod tests {
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry())
|
||||
.with_decoy(DecoyConfig::NotFound)
|
||||
.with_extra_routes(extra);
|
||||
let request = b"GET /totally/unknown HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
let request =
|
||||
b"GET /totally/unknown HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
let response = serve_and_read(adapter, request).await;
|
||||
assert!(response.starts_with("HTTP/1.1 404"), "expected 404 decoy, got: {response}");
|
||||
assert!(
|
||||
response.starts_with("HTTP/1.1 404"),
|
||||
"expected 404 decoy, got: {response}"
|
||||
);
|
||||
assert!(response.contains("404 Not Found"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn healthz_takes_precedence_over_decoy() {
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry())
|
||||
.with_decoy(DecoyConfig::Redirect {
|
||||
let adapter =
|
||||
HttpAdapter::new(provider(), empty_registry()).with_decoy(DecoyConfig::Redirect {
|
||||
to: "https://example.com".to_string(),
|
||||
});
|
||||
let request = b"GET /healthz HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
let response = serve_and_read(adapter, request).await;
|
||||
assert!(response.starts_with("HTTP/1.1 200"), "expected 200 healthz, got: {response}");
|
||||
assert!(
|
||||
response.starts_with("HTTP/1.1 200"),
|
||||
"expected 200 healthz, got: {response}"
|
||||
);
|
||||
assert!(response.contains("\r\n\r\nok"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_path_with_redirect_decoy_returns_redirect_over_wire() {
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry()).with_decoy(DecoyConfig::Redirect {
|
||||
to: "https://example.com".to_string(),
|
||||
});
|
||||
let adapter =
|
||||
HttpAdapter::new(provider(), empty_registry()).with_decoy(DecoyConfig::Redirect {
|
||||
to: "https://example.com".to_string(),
|
||||
});
|
||||
let request = b"GET /nope HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
let response = serve_and_read(adapter, request).await;
|
||||
assert!(response.starts_with("HTTP/1.1 302"), "expected 302 redirect, got: {response}");
|
||||
assert!(
|
||||
response.starts_with("HTTP/1.1 302"),
|
||||
"expected 302 redirect, got: {response}"
|
||||
);
|
||||
assert!(response.contains("location: https://example.com"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,11 +80,12 @@ where
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
_state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let identity = parts.extensions.get::<Option<Identity>>().cloned().flatten();
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let identity = parts
|
||||
.extensions
|
||||
.get::<Option<Identity>>()
|
||||
.cloned()
|
||||
.flatten();
|
||||
Ok(ResolvedIdentity(identity))
|
||||
}
|
||||
}
|
||||
@@ -174,15 +175,16 @@ mod tests {
|
||||
assert!(identity.is_none());
|
||||
}
|
||||
|
||||
async fn run_middleware(
|
||||
idp: Arc<dyn IdentityProvider>,
|
||||
request: Request,
|
||||
) -> Response {
|
||||
async fn run_middleware(idp: Arc<dyn IdentityProvider>, request: Request) -> Response {
|
||||
let app: Router<()> = Router::new()
|
||||
.route(
|
||||
"/",
|
||||
get(|req: Request| async move {
|
||||
let identity = req.extensions().get::<Option<Identity>>().cloned().flatten();
|
||||
let identity = req
|
||||
.extensions()
|
||||
.get::<Option<Identity>>()
|
||||
.cloned()
|
||||
.flatten();
|
||||
if let Some(id) = identity {
|
||||
(StatusCode::OK, id.id)
|
||||
} else {
|
||||
@@ -261,14 +263,12 @@ mod tests {
|
||||
let app: Router<()> = Router::new()
|
||||
.route(
|
||||
"/",
|
||||
get(
|
||||
|ResolvedIdentity(identity): ResolvedIdentity| async move {
|
||||
match identity {
|
||||
Some(id) => (StatusCode::OK, id.id),
|
||||
None => (StatusCode::OK, "none".to_string()),
|
||||
}
|
||||
},
|
||||
),
|
||||
get(|ResolvedIdentity(identity): ResolvedIdentity| async move {
|
||||
match identity {
|
||||
Some(id) => (StatusCode::OK, id.id),
|
||||
None => (StatusCode::OK, "none".to_string()),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.layer(from_fn_with_state(idp, bearer_auth_middleware));
|
||||
|
||||
@@ -287,14 +287,12 @@ mod tests {
|
||||
let app: Router<()> = Router::new()
|
||||
.route(
|
||||
"/",
|
||||
get(
|
||||
|ResolvedIdentity(identity): ResolvedIdentity| async move {
|
||||
match identity {
|
||||
Some(id) => (StatusCode::OK, id.id),
|
||||
None => (StatusCode::OK, "none".to_string()),
|
||||
}
|
||||
},
|
||||
),
|
||||
get(|ResolvedIdentity(identity): ResolvedIdentity| async move {
|
||||
match identity {
|
||||
Some(id) => (StatusCode::OK, id.id),
|
||||
None => (StatusCode::OK, "none".to_string()),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.layer(from_fn_with_state(idp, bearer_auth_middleware));
|
||||
|
||||
@@ -306,4 +304,4 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(&bytes[..], b"none");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,8 @@ pub fn fake_nginx_404() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
);
|
||||
resp.headers_mut().insert(
|
||||
header::SERVER,
|
||||
HeaderValue::from_static("nginx"),
|
||||
);
|
||||
resp.headers_mut()
|
||||
.insert(header::SERVER, HeaderValue::from_static("nginx"));
|
||||
resp
|
||||
}
|
||||
|
||||
@@ -61,10 +59,8 @@ pub async fn serve_static(root: &Path, request: Request) -> Response {
|
||||
let content_type = mime_for_path(&resolved);
|
||||
let mut resp = Response::new(Body::from(bytes));
|
||||
*resp.status_mut() = StatusCode::OK;
|
||||
resp.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static(content_type),
|
||||
);
|
||||
resp.headers_mut()
|
||||
.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
|
||||
resp
|
||||
}
|
||||
Err(_) => fake_nginx_404(),
|
||||
@@ -173,10 +169,7 @@ mod tests {
|
||||
async fn send(router: axum::Router, uri: &str) -> axum::response::Response {
|
||||
tower::ServiceExt::<Request<Body>>::oneshot(
|
||||
router,
|
||||
Request::builder()
|
||||
.uri(uri)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
Request::builder().uri(uri).body(Body::empty()).unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -220,9 +213,7 @@ mod tests {
|
||||
async fn unknown_path_with_static_site_decoy_serves_file() {
|
||||
let dir = tempfile_dir();
|
||||
let file = dir.join("index.html");
|
||||
tokio::fs::write(&file, "<h1>hello</h1>")
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::fs::write(&file, "<h1>hello</h1>").await.unwrap();
|
||||
|
||||
let decoy = DecoyConfig::StaticSite { root: dir.clone() };
|
||||
let resp = send(decoy_router(decoy), "/").await;
|
||||
@@ -293,11 +284,9 @@ mod tests {
|
||||
}
|
||||
|
||||
fn tempfile_dir() -> PathBuf {
|
||||
let dir = PathBuf::from("/tmp").join(format!(
|
||||
"alknet-http-decoy-test-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let dir =
|
||||
PathBuf::from("/tmp").join(format!("alknet-http-decoy-test-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,13 +52,19 @@ impl GatewayState {
|
||||
}
|
||||
|
||||
fn dispatch(&self) -> GatewayDispatch {
|
||||
GatewayDispatch::new(Arc::clone(&self.registry), Arc::clone(&self.identity_provider))
|
||||
GatewayDispatch::new(
|
||||
Arc::clone(&self.registry),
|
||||
Arc::clone(&self.identity_provider),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<RouterState> for GatewayState {
|
||||
fn from_ref(state: &RouterState) -> Self {
|
||||
GatewayState::new(Arc::clone(&state.registry), Arc::clone(&state.identity_provider))
|
||||
GatewayState::new(
|
||||
Arc::clone(&state.registry),
|
||||
Arc::clone(&state.identity_provider),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +98,9 @@ pub(crate) async fn call_handler(
|
||||
return not_found_response(&request.operation);
|
||||
}
|
||||
let dispatch = state.dispatch();
|
||||
let envelope = dispatch.invoke(identity.clone(), &request.operation, request.input).await;
|
||||
let envelope = dispatch
|
||||
.invoke(identity.clone(), &request.operation, request.input)
|
||||
.await;
|
||||
envelope_to_response(envelope, identity.as_ref())
|
||||
}
|
||||
|
||||
@@ -101,7 +109,9 @@ pub(crate) async fn search_handler(
|
||||
ResolvedIdentity(identity): ResolvedIdentity,
|
||||
) -> Response {
|
||||
let dispatch = state.dispatch();
|
||||
let envelope = dispatch.invoke(identity.clone(), SERVICES_LIST, json!({})).await;
|
||||
let envelope = dispatch
|
||||
.invoke(identity.clone(), SERVICES_LIST, json!({}))
|
||||
.await;
|
||||
envelope_to_response(envelope, identity.as_ref())
|
||||
}
|
||||
|
||||
@@ -115,7 +125,11 @@ pub(crate) async fn schema_handler(
|
||||
}
|
||||
let dispatch = state.dispatch();
|
||||
let envelope = dispatch
|
||||
.invoke(identity.clone(), SERVICES_SCHEMA, json!({ "name": query.name }))
|
||||
.invoke(
|
||||
identity.clone(),
|
||||
SERVICES_SCHEMA,
|
||||
json!({ "name": query.name }),
|
||||
)
|
||||
.await;
|
||||
envelope_to_response(envelope, identity.as_ref())
|
||||
}
|
||||
@@ -149,7 +163,9 @@ pub(crate) async fn subscribe_handler(
|
||||
subscribe_stream_internal_error(request.operation)
|
||||
} else {
|
||||
let dispatch = state.dispatch();
|
||||
let envelope = dispatch.invoke(identity, &request.operation, request.input).await;
|
||||
let envelope = dispatch
|
||||
.invoke(identity, &request.operation, request.input)
|
||||
.await;
|
||||
subscribe_stream_from_envelope(envelope)
|
||||
};
|
||||
Sse::new(stream)
|
||||
@@ -221,8 +237,7 @@ fn not_found_response(operation: &str) -> Response {
|
||||
fn forbidden_response(message: String, identity: Option<&Identity>) -> Response {
|
||||
let error = CallError::forbidden(message);
|
||||
let status_code = call_error_to_http_status_with_identity(&error, identity);
|
||||
let status =
|
||||
StatusCode::from_u16(status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
let status = StatusCode::from_u16(status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
let body = serde_json::to_value(&error).unwrap_or(Value::Null);
|
||||
(status, Json(body)).into_response()
|
||||
}
|
||||
@@ -248,7 +263,9 @@ fn is_internal_op(registry: &OperationRegistry, operation: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn envelope_to_sse_stream(envelope: ResponseEnvelope) -> impl Stream<Item = Result<Event, Infallible>> {
|
||||
fn envelope_to_sse_stream(
|
||||
envelope: ResponseEnvelope,
|
||||
) -> impl Stream<Item = Result<Event, Infallible>> {
|
||||
stream::once(async move {
|
||||
match envelope.result {
|
||||
Ok(output) => {
|
||||
@@ -756,7 +773,10 @@ mod tests {
|
||||
.get(axum::http::header::CONTENT_TYPE)
|
||||
.map(|v| v.to_str().unwrap().to_string());
|
||||
assert!(
|
||||
ctype.as_deref().unwrap_or("").starts_with("text/event-stream"),
|
||||
ctype
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.starts_with("text/event-stream"),
|
||||
"expected text/event-stream, got {ctype:?}"
|
||||
);
|
||||
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
|
||||
@@ -950,4 +970,4 @@ mod tests {
|
||||
assert_eq!(status, StatusCode::NOT_FOUND);
|
||||
assert_eq!(body.get("code"), Some(&json!("NOT_FOUND")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,4 +59,4 @@ mod tests {
|
||||
let resp = call_healthz(req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user