Skip to content

Commit 2e1e61c

Browse files
committed
Refine request-reply transport support and webhook protocol detection
1 parent 2b3d5cf commit 2e1e61c

19 files changed

Lines changed: 229 additions & 289 deletions

File tree

eventmesh-sdks/eventmesh-sdk-rust/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ See the runnable transport-specific consumer programs in [examples/README.md](ex
7373

7474
`HttpClient::consumer` binds its callback socket before registering subscriptions, then owns the axum server, heartbeat, and registration lifecycle. For an application-owned endpoint, use `HttpClient::webhook_registration` with `eventmesh::http::codec::{parse_push_body, WebhookReply}`. TCP unsubscribe is session-wide, so its API is `unsubscribe_all()`.
7575

76-
`HttpProducer::request_reply` encodes EventMesh's HTTP synchronous-publish request, but the current stock Runtime cannot route an HTTP-originated synchronous message through a gRPC stream consumer and return its reply to the HTTP request. The corresponding real-Runtime e2e is retained but ignored to make this compatibility gap visible. Use gRPC or TCP for request/reply unless the target deployment provides a compatible HTTP synchronous-reply path.
76+
HTTP request/reply is not exposed because the current SDK and stock Runtime do not provide a complete HTTP responder path. Use gRPC or TCP for request/reply.
7777

7878
`Message` is a public dialect envelope, not a wire format. The selected transport owns protobuf, HTTP form, or TCP frame serialization. With `cloud_events`, CloudEvents remain CloudEvents; `Message::into_event_mesh()` does not silently flatten them into the native EventMesh model.
7979

@@ -83,7 +83,7 @@ See the runnable transport-specific consumer programs in [examples/README.md](ex
8383

8484
All configurations require a validated `Endpoint`; HTTP uses a non-empty `EndpointSet`. Use `with_*` methods to set optional identity, credentials, timeouts, HTTP TLS, proxy, and reconnect settings. EventMesh Runtime's gRPC endpoint is plaintext and the gRPC client intentionally does not expose TLS configuration. `Debug` output redacts secrets.
8585

86-
Default request timeouts are 5 seconds (gRPC), 15 seconds (HTTP), and 20 seconds (TCP). `ClientOptions::with_request_timeout` changes a client's default; every producer also has `request_reply_with_timeout` for one call. TCP separately has a 1-second connect timeout and a 20-second control timeout.
86+
Default request timeouts are 5 seconds (gRPC), 15 seconds (HTTP), and 20 seconds (TCP). `ClientOptions::with_request_timeout` changes a client's default; gRPC and TCP producers also have `request_reply_with_timeout` for one call. TCP separately has a 1-second connect timeout and a 20-second control timeout.
8787

8888
Operations return the pattern-matchable `eventmesh::Error`; common variants include `Config`, `InvalidArgument`, `InvalidMessage`, `Timeout`, `Server`, `Protocol`, `Unsupported`, and transport-specific errors.
8989

eventmesh-sdks/eventmesh-sdk-rust/src/config/client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,7 @@ mod tests {
10831083
.is_err());
10841084
}
10851085

1086+
#[cfg(any(feature = "grpc", feature = "http", feature = "tcp"))]
10861087
#[test]
10871088
fn role_groups_must_not_be_blank() {
10881089
assert!(ProducerOptions::new(" ").validate().is_err());

eventmesh-sdks/eventmesh-sdk-rust/src/grpc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::transport::grpc::{
2626
GrpcClient as ChannelClient, GrpcProducer as LegacyProducer,
2727
GrpcStreamConsumer as LegacyConsumer, GrpcWebhookConsumer as LegacyWebhookConsumer,
2828
};
29-
use crate::transport::Publisher as LegacyPublisher;
29+
use crate::transport::{Publisher as LegacyPublisher, RequestReply as LegacyRequestReply};
3030
use crate::MessageHandler;
3131

3232
/// A configured EventMesh gRPC client.

eventmesh-sdks/eventmesh-sdk-rust/src/http.rs

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ impl HttpClient {
6262
options.validate()?;
6363
Ok(HttpProducer {
6464
inner: LegacyProducer::new(self.config.legacy(Some(&options), None))?,
65-
timeout: self.config.request_timeout(),
6665
})
6766
}
6867

@@ -155,7 +154,6 @@ impl HttpClient {
155154
/// HTTP publishing capability.
156155
pub struct HttpProducer {
157156
inner: LegacyProducer,
158-
timeout: std::time::Duration,
159157
}
160158

161159
impl HttpProducer {
@@ -175,50 +173,6 @@ impl HttpProducer {
175173
.map(PublishReceipt::from_legacy),
176174
}
177175
}
178-
179-
/// Send an event and await its reply.
180-
///
181-
/// # Runtime compatibility
182-
///
183-
/// The SDK implements EventMesh's HTTP synchronous-publish wire request,
184-
/// but the current EventMesh Runtime cannot route an HTTP-originated
185-
/// synchronous message through a gRPC stream consumer and return that
186-
/// consumer's reply to the HTTP request. Against the stock Runtime this
187-
/// operation can therefore fail or time out; use gRPC or TCP when a
188-
/// verified request/reply path is required. Keep this method only for
189-
/// deployments that provide a compatible HTTP synchronous-reply path.
190-
pub async fn request_reply(&self, message: Message) -> Result<Message> {
191-
self.request_reply_with_timeout(message, self.timeout).await
192-
}
193-
194-
/// Send an event and await its reply with a per-operation timeout.
195-
///
196-
/// This has the same current Runtime limitation documented on
197-
/// [`request_reply`](Self::request_reply).
198-
pub async fn request_reply_with_timeout(
199-
&self,
200-
message: Message,
201-
timeout: std::time::Duration,
202-
) -> Result<Message> {
203-
if timeout.is_zero() {
204-
return Err(EventMeshError::InvalidArgument(
205-
"request/reply timeout must be greater than zero".into(),
206-
));
207-
}
208-
match message {
209-
Message::EventMesh(message) => self
210-
.inner
211-
.request_reply(message, timeout)
212-
.await
213-
.map(Message::EventMesh),
214-
#[cfg(feature = "cloud_events")]
215-
Message::CloudEvent(event) => self
216-
.inner
217-
.request_reply_cloud_event(event, timeout)
218-
.await
219-
.map(Message::CloudEvent),
220-
}
221-
}
222176
}
223177

224178
/// An SDK-managed HTTP consumer with an embedded axum callback server.

eventmesh-sdks/eventmesh-sdk-rust/src/tcp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::subscription::Subscription;
2525
use crate::transport::tcp::{
2626
TcpConsumer as LegacyConsumer, TcpMessage, TcpProducer as LegacyProducer,
2727
};
28-
use crate::transport::Publisher as LegacyPublisher;
28+
use crate::transport::{Publisher as LegacyPublisher, RequestReply as LegacyRequestReply};
2929
use crate::MessageHandler;
3030
use tracing::warn;
3131

eventmesh-sdks/eventmesh-sdk-rust/src/transport/grpc/producer.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::error::{EventMeshError, Result};
2525
use crate::model::{EventMeshMessage, PublishResponse};
2626
use crate::transport::grpc::client::GrpcClient;
2727
use crate::transport::grpc::codec;
28-
use crate::transport::Publisher;
28+
use crate::transport::{Publisher, RequestReply};
2929

3030
/// gRPC-based producer.
3131
pub struct GrpcProducer {
@@ -204,7 +204,9 @@ impl Publisher for GrpcProducer {
204204
}
205205
Ok(response)
206206
}
207+
}
207208

209+
impl RequestReply for GrpcProducer {
208210
async fn request_reply(
209211
&self,
210212
message: EventMeshMessage,

eventmesh-sdks/eventmesh-sdk-rust/src/transport/http/codec.rs

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -418,36 +418,6 @@ pub fn parse_response(body: &str) -> Result<PublishResponse> {
418418
Ok(obj.into())
419419
}
420420

421-
/// The reply payload returned inside the `retMsg` field of a request-reply
422-
/// `EventMeshRetObj`.
423-
///
424-
/// Mirrors `SendMessageResponseBody.ReplyMessage` on the Java side:
425-
/// `topic`, `body`, and `properties`.
426-
#[derive(Debug, Clone, Deserialize)]
427-
pub struct ReplyMessage {
428-
#[serde(default)]
429-
pub topic: Option<String>,
430-
#[serde(default)]
431-
pub body: Option<String>,
432-
#[serde(default)]
433-
pub properties: HashMap<String, String>,
434-
}
435-
436-
/// Parse the reply message from the `retMsg` field of a request-reply
437-
/// response, mapping `body` → `content`, `topic` → `topic`, and
438-
/// `properties` → `props`.
439-
///
440-
/// Mirrors the Java SDK's `EventMeshMessageProducer.transformMessage`, which
441-
/// deserializes `retMsg` as a `SendMessageResponseBody.ReplyMessage`.
442-
pub fn parse_reply(ret_msg: &str) -> Result<EventMeshMessage> {
443-
let reply: ReplyMessage = serde_json::from_str(ret_msg)?;
444-
Ok(EventMeshMessage::builder()
445-
.topic(reply.topic.unwrap_or_default())
446-
.content(reply.body.unwrap_or_default())
447-
.props(reply.properties)
448-
.build())
449-
}
450-
451421
/// Form-encode a list of `(key, value)` pairs into a URL-encoded body string.
452422
pub fn form_encode(fields: &[(String, String)]) -> String {
453423
serde_urlencoded::to_string(fields).unwrap_or_default()
@@ -458,11 +428,6 @@ pub fn publish_code() -> i32 {
458428
RequestCode::MSG_SEND_ASYNC
459429
}
460430

461-
/// Request code for synchronous request-reply (code-based routing).
462-
pub fn publish_sync_code() -> i32 {
463-
RequestCode::MSG_SEND_SYNC
464-
}
465-
466431
pub fn subscribe_code() -> i32 {
467432
RequestCode::SUBSCRIBE
468433
}
@@ -710,12 +675,6 @@ mod tests {
710675
assert!(!headers.iter().any(|(k, _)| *k == "token"));
711676
}
712677

713-
#[test]
714-
fn publish_sync_code_is_101() {
715-
assert_eq!(publish_sync_code(), RequestCode::MSG_SEND_SYNC);
716-
assert_eq!(publish_sync_code(), 101);
717-
}
718-
719678
#[test]
720679
fn parse_response_success() {
721680
let body = r#"{"retCode":0,"retMsg":"success","resTime":42}"#;
@@ -741,15 +700,6 @@ mod tests {
741700
assert!(parse_response(body).is_err());
742701
}
743702

744-
#[test]
745-
fn parse_reply_from_ret_msg() {
746-
let ret_msg = r#"{"topic":"reply-topic","body":"reply-body","properties":{"k":"v"}}"#;
747-
let msg = parse_reply(ret_msg).unwrap();
748-
assert_eq!(msg.topic.as_deref(), Some("reply-topic"));
749-
assert_eq!(msg.content.as_deref(), Some("reply-body"));
750-
assert_eq!(msg.get_prop("k"), Some("v"));
751-
}
752-
753703
#[test]
754704
fn parse_push_body_form_urlencoded() {
755705
let body = "content=hello&topic=test-topic&bizseqno=seq1";

eventmesh-sdks/eventmesh-sdk-rust/src/transport/http/producer.rs

Lines changed: 1 addition & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
//! HTTP producer.
1919
20-
use std::time::Duration;
21-
2220
use tracing::debug;
2321

2422
use crate::config::HttpClientConfig;
@@ -30,8 +28,7 @@ use crate::transport::Publisher;
3028

3129
/// HTTP-based producer.
3230
///
33-
/// Implements fire-and-forget publish, batch publish, and synchronous
34-
/// request-reply over the EventMesh HTTP protocol.
31+
/// Implements publishing over the EventMesh HTTP protocol.
3532
pub struct HttpProducer {
3633
client: EventMeshHttpClient,
3734
}
@@ -75,38 +72,6 @@ impl HttpProducer {
7572
.await
7673
}
7774

78-
/// Send a native CloudEvent and wait for a native CloudEvent reply.
79-
///
80-
/// The current Runtime cannot complete the HTTP-originated synchronous
81-
/// request through its gRPC stream-consumer reply path. This codec remains
82-
/// for deployments that provide a compatible HTTP synchronous-reply path.
83-
#[cfg(feature = "cloud_events")]
84-
pub async fn request_reply_cloud_event(
85-
&self,
86-
mut event: cloudevents::Event,
87-
timeout: Duration,
88-
) -> Result<cloudevents::Event> {
89-
use cloudevents::AttributesReader;
90-
91-
ensure_ce_extension(&mut event, "bizseqno");
92-
ensure_ce_extension(&mut event, "uniqueid");
93-
ensure_ce_ttl(&mut event);
94-
let topic = event
95-
.subject()
96-
.ok_or_else(|| {
97-
EventMeshError::InvalidMessage("CloudEvent subject (topic) is required".into())
98-
})?
99-
.to_string();
100-
let message = EventMeshMessage::builder()
101-
.topic(topic)
102-
.content(serde_json::to_string(&event)?)
103-
.build();
104-
let reply = self
105-
.request_reply_with_protocol(message, timeout, EventMeshProtocolType::CloudEvents)
106-
.await?;
107-
decode_cloud_event_reply(reply)
108-
}
109-
11075
/// Internal publish with a specific protocol type.
11176
async fn publish_with_protocol(
11277
&self,
@@ -133,33 +98,6 @@ impl HttpProducer {
13398
debug!("published topic={:?}", message.topic);
13499
Ok(response)
135100
}
136-
137-
async fn request_reply_with_protocol(
138-
&self,
139-
message: EventMeshMessage,
140-
timeout: Duration,
141-
protocol_type: EventMeshProtocolType,
142-
) -> Result<EventMeshMessage> {
143-
message.validate_for_publish()?;
144-
let config = self.client.config();
145-
let body = codec::encode_publish(&message, &config.identity);
146-
let headers =
147-
codec::build_headers(codec::publish_sync_code(), protocol_type, &config.identity);
148-
let text = self
149-
.client
150-
.post_form(uri::ROOT, &body, &headers, timeout)
151-
.await?;
152-
let response = codec::parse_response(&text)?;
153-
if !response.is_success() {
154-
return Err(EventMeshError::Server {
155-
code: response.code.unwrap_or(-1) as i32,
156-
message: response
157-
.message
158-
.unwrap_or_else(|| "request-reply failed".into()),
159-
});
160-
}
161-
codec::parse_reply(response.message.as_deref().unwrap_or(""))
162-
}
163101
}
164102

165103
impl Publisher for HttpProducer {
@@ -180,39 +118,6 @@ impl Publisher for HttpProducer {
180118
.into(),
181119
))
182120
}
183-
184-
async fn request_reply(
185-
&self,
186-
message: EventMeshMessage,
187-
timeout: Duration,
188-
) -> Result<EventMeshMessage> {
189-
self.request_reply_with_protocol(message, timeout, EventMeshProtocolType::EventMeshMessage)
190-
.await
191-
}
192-
}
193-
194-
/// Decode the HTTP reply payload into a native CloudEvent. EventMesh
195-
/// installations return either CloudEvents JSON directly or the legacy
196-
/// `ReplyMessage` envelope; accepting both keeps interop with Java runtimes.
197-
#[cfg(feature = "cloud_events")]
198-
fn decode_cloud_event_reply(reply: EventMeshMessage) -> Result<cloudevents::Event> {
199-
if let Some(content) = &reply.content {
200-
if let Ok(event) = serde_json::from_str(content) {
201-
return Ok(event);
202-
}
203-
}
204-
use cloudevents::{EventBuilder, EventBuilderV10};
205-
let topic = reply.topic.unwrap_or_default();
206-
EventBuilderV10::new()
207-
.id(reply
208-
.unique_id
209-
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()))
210-
.source("eventmesh://http-reply")
211-
.ty("eventmesh.reply")
212-
.subject(topic)
213-
.data("text/plain", reply.content.unwrap_or_default())
214-
.build()
215-
.map_err(|e| EventMeshError::InvalidMessage(format!("invalid CloudEvent reply: {e}")))
216121
}
217122

218123
/// Ensure a CloudEvent extension attribute is present and non-blank,

0 commit comments

Comments
 (0)