DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

Response-Building-and-Sending

Response Building and Sending in Hyperlane

Project Code:https://github.com/hyperlane-dev/hyperlane

Introduction

In the hyperlane framework, response building and sending are core operations that every developer must master. Unlike many web frameworks that abstract these details away, hyperlane gives you fine-grained control over how HTTP responses are constructed and delivered to clients. This article explores the comprehensive API for building responses, setting headers, managing status codes, and efficiently sending data back to clients.

Hyperlane is a lightweight, high-performance, cross-platform Rust HTTP server library built on top of Tokio. Its response system is designed for both simplicity and flexibility, supporting method chaining, attribute macros, and multiple sending strategies.

Setting Response Basics

HTTP Version and Status Code

Every HTTP response starts with a version and a status code. Hyperlane provides a fluent API for setting these fundamental properties:

ctx.get_mut_response().set_version(HttpVersion::Http1_1).set_status_code(200);
Enter fullscreen mode Exit fullscreen mode

This sets the response to HTTP/1.1 with a 200 OK status. The HttpVersion enum supports both HTTP/1.1 and HTTP/1.0, giving you control over protocol negotiation.

Response Body

The response body can be set with a simple string:

ctx.get_mut_response().set_body("Hello World");
Enter fullscreen mode Exit fullscreen mode

This stores the body content internally in the response object. The body is not sent to the client until you explicitly call the build() method and send the resulting data through the stream.

Response Headers

Hyperlane provides two methods for setting response headers:

ctx.get_mut_response().set_header(CONTENT_TYPE, APPLICATION_JSON);
ctx.get_mut_response().add_header(SERVER, "hyperlane");
Enter fullscreen mode Exit fullscreen mode
  • set_header — Sets a header value, replacing any existing value for the same key.
  • add_header — Appends a header value, allowing multiple values for the same header key (useful for Set-Cookie headers).

These methods use the HeaderName constants defined in hyperlane's HTTP module, ensuring type safety and preventing typos.

Building the Response

The build() Method

The build() method serializes the response into a byte buffer that can be sent over the network:

let data = ctx.get_mut_response().build();
Enter fullscreen mode Exit fullscreen mode

This method takes all the configured properties — version, status code, headers, and body — and produces a complete HTTP response in binary format. The resulting data is ready to be sent through the stream.

It is important to note that build() should be called after all response properties have been configured. Any changes made to the response after calling build() will not be reflected in the already-built data.

Sending the Response

Hyperlane offers multiple strategies for sending response data, each with different trade-offs:

try_send — Non-Blocking Send

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

try_send attempts to send the data without blocking. If the stream's internal buffer is full, it returns an error immediately rather than waiting. This is useful when you want to handle backpressure 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

send will wait until the data can be sent, blocking the current task if necessary. Use this when you need guaranteed delivery and can tolerate potential waiting.

try_send_list — Batch Sending

stream.try_send_list(&frame_list).await;
Enter fullscreen mode Exit fullscreen mode

For scenarios where you need to send multiple frames at once (such as WebSocket messages or SSE events), try_send_list efficiently sends a collection of data frames in a single operation.

try_flush — Flushing the Stream

stream.try_flush().await;
Enter fullscreen mode Exit fullscreen mode

After sending data, calling try_flush ensures that all buffered data is pushed to the underlying socket immediately. This is particularly important for SSE and streaming responses where data needs to reach the client without delay.

Attribute Macros for Response Configuration

Hyperlane's attribute macro system provides a declarative approach to response configuration, making your code more concise and readable.

Setting Status Code and Version

#[response_status_code(200)]
#[response_version(HttpVersion::Http1_1)]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
    // Response is automatically configured with 200 OK and HTTP/1.1
    Status::Continue
}
Enter fullscreen mode Exit fullscreen mode

Setting Response Body

#[response_body("Hello World")]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
    // The response body is automatically set to "Hello World"
    Status::Continue
}
Enter fullscreen mode Exit fullscreen mode

Setting Response Headers

#[response_header(SERVER => HYPERLANE)]
#[response_header(SET_COOKIE, "session_id=abc123")]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
    // Headers are automatically added to the response
    Status::Continue
}
Enter fullscreen mode Exit fullscreen mode

Clearing Response Headers

#[clear_response_headers]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
    // All existing response headers are cleared
    Status::Continue
}
Enter fullscreen mode Exit fullscreen mode

Attribute Macros for Sending

Hyperlane also provides attribute macros for the sending phase:

#[try_send]
#[send]
#[try_flush]
#[flush]
#[closed]
Enter fullscreen mode Exit fullscreen mode

These macros can be applied to handler methods to automatically perform the corresponding stream operation after the handler completes. For example, #[try_send] will automatically build and send the response through the stream.

Complete Response Lifecycle Example

Here is a complete example showing the full response lifecycle in a custom middleware:

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 {
        // Step 1: Configure the response
        ctx.get_mut_response()
            .set_version(HttpVersion::Http1_1)
            .set_status_code(200);

        // Step 2: Set the body
        ctx.get_mut_response().set_body("Hello World");

        // Step 3: Add headers
        ctx.get_mut_response().set_header(CONTENT_TYPE, APPLICATION_JSON);
        ctx.get_mut_response().add_header(SERVER, "hyperlane");

        // Step 4: Build the response into bytes
        let data = ctx.get_mut_response().build();

        // Step 5: Send the response, handling potential errors
        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

Combining Response Building with Other Features

Response Building in Route Handlers

Response building integrates seamlessly with route handlers. You can use route parameters and request data to construct dynamic responses:

#[route("/test/{text}")]
struct Route;

impl ServerHook for Route {
    async fn new(_: &mut Stream, _: &mut Context) -> Self {
        Self
    }

    async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
        let param: String = ctx.get_route_param("text");
        let body = format!("Hello, {}!", param);

        ctx.get_mut_response()
            .set_version(HttpVersion::Http1_1)
            .set_status_code(200)
            .set_body(body);

        let data = ctx.get_mut_response().build();
        stream.try_send(data).await;

        Status::Continue
    }
}
Enter fullscreen mode Exit fullscreen mode

Response Building with Cookies

Response building works hand-in-hand with cookie management. After building a cookie with CookieBuilder, you attach it to the response headers:

let cookie = CookieBuilder::new("session_id", "abc123")
    .set_path("/")
    .http_only()
    .build();

ctx.get_mut_response().set_header(SET_COOKIE, &cookie);
Enter fullscreen mode Exit fullscreen mode

Response Building with SSE

For Server-Sent Events, the response building process is slightly different. You set the content type to text/event-stream and initialize with an empty body:

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

Best Practices

  1. Always call build() after all properties are configured. Changes made after build() are not reflected in the serialized data.

  2. Use try_send for non-critical responses where you can gracefully handle backpressure. Use send when guaranteed delivery is required.

  3. Prefer attribute macros for simple response configurations to keep your code clean and maintainable.

  4. Handle send errors gracefully. Always check the return value of try_send and close the stream if necessary using stream.set_closed(true).

  5. Flush after sending streaming data. For SSE and long-polling scenarios, call try_flush() to ensure data reaches the client promptly.

  6. Set appropriate headers before building. Remember that the order of header operations matters — set_header replaces while add_header appends.

Conclusion

Hyperlane's response building and sending system provides a powerful and flexible foundation for constructing HTTP responses. Whether you use the fluent method chain API or the declarative attribute macros, you have full control over every aspect of the response lifecycle. The multiple sending strategies — try_send, send, try_send_list, and try_flush — ensure that you can handle everything from simple request-response patterns to complex streaming scenarios with ease.

By mastering these APIs, you can build robust, high-performance web applications that take full advantage of hyperlane's speed and efficiency.


Project Code:https://github.com/hyperlane-dev/hyperlane

Top comments (0)