Sending Responses and Flushing in Hyperlane
Project Code:https://github.com/hyperlane-dev/hyperlane
Introduction
In Hyperlane, the response lifecycle doesn't end when you set the status code and body. The framework gives you fine-grained control over when and how response data is actually sent to the client. This article covers the response building, sending, and flushing mechanisms, along with the attribute macros that streamline these operations.
Building a Response
Before sending a response, you need to build it. The build() method serializes the response data into bytes ready for transmission:
let data = ctx.get_mut_response().build();
This takes the current state of the response (version, status code, headers, body) and produces a serialized byte buffer. The build() method is idempotent — you can call it multiple times if needed, and each call produces the same output based on the current response state.
Sending Response Data
Hyperlane provides two primary methods for sending response data over the stream:
try_send — Non-blocking Send
stream.try_send(data).await;
The try_send method attempts to send the data and returns immediately. If the send fails (e.g., due to a closed connection), it returns an error. This is the preferred method for most use cases because it allows you to handle failures gracefully:
let data = ctx.get_mut_response().build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
return Status::Reject;
}
send — Blocking Send
stream.send(data).await;
The send method blocks until the data is sent. Use this when you need to guarantee delivery and are willing to wait. It's simpler to use but can potentially block the task if the client is slow.
Sending Multiple Frames
For protocols like WebSocket where you need to send multiple frames at once, Hyperlane provides:
stream.try_send_list(&frame_list).await;
This sends a list of frames in a single call, which is more efficient than sending each frame individually. For example, in a WebSocket handler:
let body_list = WebSocketFrame::create_frame_list(&body);
stream.send_list(body_list).await;
Flushing the Stream
After sending data, you may want to ensure it's actually pushed to the network rather than sitting in a buffer:
try_flush — Non-blocking Flush
stream.try_flush().await;
flush — Blocking Flush
#[flush]
Flushing is important when you want to ensure the client receives data immediately rather than waiting for the buffer to fill. This is particularly relevant for SSE (Server-Sent Events) and streaming responses.
Closing the Connection
When you're done sending data and want to signal that no more data will follow:
stream.set_closed(true);
This is typically called after an error or when the response is complete and keep-alive is not desired.
Attribute Macros for Sending
Hyperlane provides attribute macros that automate the send and flush lifecycle. These can be applied to handler functions:
#[try_send]
#[send]
#[try_flush]
#[flush]
#[closed]
When you annotate a handler with #[try_send], Hyperlane automatically calls try_send with the built response after the handler completes. Similarly, #[send] calls send, #[try_flush] calls try_flush, #[flush] calls flush, and #[closed] closes the stream.
This dramatically reduces boilerplate. Compare the manual approach:
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let data = ctx.get_mut_response().build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
return Status::Reject;
}
Status::Continue
}
With the macro approach:
#[try_send]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
// Response is automatically sent after this function
Status::Continue
}
SSE Example: Sending and Flushing in Action
Server-Sent Events (SSE) is a perfect example of why sending and flushing matter. In SSE, you need to send data incrementally and flush after each event:
let data = ctx.get_mut_response()
.set_header(CONTENT_TYPE, TEXT_EVENT_STREAM)
.set_body(Vec::new())
.build();
stream.try_send(data).await;
for i in 0..10 {
let body = format!("data:{i}{HTTP_DOUBLE_BR}");
stream.try_send(&body).await;
}
Here, try_send is called multiple times — once for the initial headers and once for each event. Each call sends data to the client immediately, which is essential for real-time streaming.
WebSocket Example: Sending Frame Lists
In WebSocket handlers, you often need to send entire frame lists:
#[route("/ws_upgrade_type")]
struct Websocket;
impl ServerHook for Websocket {
async fn new(_: &mut Stream, _: &mut Context) -> Self {
Self
}
#[is_ws_upgrade_type]
#[try_get_websocket_request(body)]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let body_list = WebSocketFrame::create_frame_list(&body);
stream.send_list(body_list).await;
Status::Continue
}
}
The send_list method efficiently sends all frames at once, reducing the number of system calls.
Error Handling During Sends
When try_send fails, it usually means the client has disconnected. The standard pattern is:
let data = ctx.get_mut_response()
.set_status_code(500)
.set_body("error")
.build();
stream.try_send(data).await;
In error-handling middleware, you might not always be able to send the error response if the connection is already broken:
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let data = ctx.get_mut_response()
.set_status_code(500)
.set_body("error")
.build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
return Status::Reject;
}
Status::Continue
}
Response Building Best Practices
-
Build once, send once — Call
build()right before sending to ensure the data is up to date. -
Use
try_sendoversend— It allows graceful error handling without blocking. - Flush when streaming — For SSE and streaming responses, flush after each chunk to ensure real-time delivery.
-
Close on error — Always call
stream.set_closed(true)whentry_sendfails to prevent further writes on a broken connection. -
Leverage attribute macros — Use
#[try_send]and#[flush]to reduce boilerplate in simple handlers.
Combining Sending with Middleware
In middleware, you often want to send the response after all request processing is complete. The response middleware pattern handles this:
struct ResponseMiddleware;
impl ServerHook for ResponseMiddleware {
async fn new(_: &mut Stream, _: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let data = ctx.get_mut_response().build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
return Status::Reject;
}
Status::Continue
}
}
server.response_middleware::<ResponseMiddleware>();
This middleware is registered as a response middleware and sends the final response after all request middleware and handlers have completed.
Conclusion
Hyperlane's response sending and flushing mechanisms give you precise control over data transmission. Whether you're building simple request-response APIs, streaming real-time events via SSE, or managing WebSocket frame lists, understanding how to build, send, and flush responses is essential. The attribute macros further simplify common patterns, letting you focus on business logic rather than boilerplate.
Project Code:https://github.com/hyperlane-dev/hyperlane
Top comments (0)