DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

Sending-Responses-and-Flushing

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();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

send — Blocking Send

stream.send(data).await;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

flush — Blocking Flush

#[flush]
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Response Building Best Practices

  1. Build once, send once — Call build() right before sending to ensure the data is up to date.
  2. Use try_send over send — It allows graceful error handling without blocking.
  3. Flush when streaming — For SSE and streaming responses, flush after each chunk to ensure real-time delivery.
  4. Close on error — Always call stream.set_closed(true) when try_send fails to prevent further writes on a broken connection.
  5. 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>();
Enter fullscreen mode Exit fullscreen mode

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)