DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

Server-Configuration

Static File Serving in Hyperlane

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

Introduction

Serving static files — HTML pages, CSS stylesheets, JavaScript files, images, and other assets — is a fundamental requirement for most web applications. Whether you are building a traditional server-rendered website or a single-page application with an API backend, the ability to efficiently deliver static content is essential.

In the hyperlane framework, static file serving is implemented as middleware that intercepts requests, reads files from the filesystem, and sends them to the client with appropriate content types. This article covers everything you need to know about serving static files in hyperlane.

Understanding Static File Serving

Static file serving involves reading files from the server's filesystem and delivering them to clients over HTTP. The key components are:

  1. Path resolution — Mapping URL paths to filesystem paths.
  2. Content type detection — Determining the correct MIME type for each file.
  3. File reading — Reading file contents from disk.
  4. Response delivery — Sending the file content with appropriate headers.

Hyperlane provides the building blocks for all of these through its FileExtension utility and the standard response building API.

Basic Static File Serving

Core Implementation

The fundamental static file serving pattern in hyperlane involves extracting the request path, resolving it to a filesystem path, reading the file, and setting the appropriate content type:

let file_path = format!("{static_path}{path}");
let file_extension = FileExtension::get_extension_name(&file_path);
let content_type = FileExtension::parse(&file_extension).get_content_type();
let file_data = tokio::fs::read_to_string(&file_path).await.unwrap();
ctx.get_mut_response().set_body(file_data);
Enter fullscreen mode Exit fullscreen mode

This code:

  1. Constructs the full filesystem file_path by combining a base directory with the request path.
  2. Extracts the file extension using FileExtension::get_extension_name.
  3. Looks up the MIME content type using FileExtension::parse.
  4. Reads the file contents asynchronously using Tokio's read_to_string.
  5. Sets the response body to the file contents.

Content Type Detection

Hyperlane's FileExtension utility handles MIME type detection automatically. It maps common file extensions to their corresponding content types:

  • .htmltext/html
  • .csstext/css
  • .jsapplication/javascript
  • .jsonapplication/json
  • .pngimage/png
  • .jpg / .jpegimage/jpeg
  • .gifimage/gif
  • .svgimage/svg+xml
  • .icoimage/x-icon
  • .txttext/plain

This automatic detection ensures that browsers receive files with the correct content type headers.

Complete Static File Middleware

Here is a complete middleware implementation for serving static files:

struct StaticFileMiddleware {
    static_path: String,
}

impl ServerHook for StaticFileMiddleware {
    async fn new(_: &mut Stream, _: &mut Context) -> Self {
        Self {
            static_path: "./static/".to_string(),
        }
    }

    async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
        let path = ctx.get_request().get_path();

        // Resolve the file path
        let file_path = format!("{static_path}{path}");

        // Get the file extension and content type
        let file_extension = FileExtension::get_extension_name(&file_path);
        let content_type = FileExtension::parse(&file_extension).get_content_type();

        // Read the file
        match tokio::fs::read_to_string(&file_path).await {
            Ok(file_data) => {
                ctx.get_mut_response()
                    .set_version(HttpVersion::Http1_1)
                    .set_status_code(200)
                    .set_header(CONTENT_TYPE, &content_type)
                    .set_body(file_data);

                let data = ctx.get_mut_response().build();
                if stream.try_send(data).await.is_err() {
                    stream.set_closed(true);
                }
            }
            Err(_) => {
                // File not found
                ctx.get_mut_response()
                    .set_version(HttpVersion::Http1_1)
                    .set_status_code(404)
                    .set_body("File not found");

                let data = ctx.get_mut_response().build();
                if stream.try_send(data).await.is_err() {
                    stream.set_closed(true);
                }
            }
        }

        Status::Continue
    }
}

server.request_middleware::<StaticFileMiddleware>();
Enter fullscreen mode Exit fullscreen mode

This middleware:

  1. Resolves the URL path to a filesystem path.
  2. Determines the content type from the file extension.
  3. Reads the file asynchronously.
  4. Returns a 200 response with the file content on success.
  5. Returns a 404 response if the file does not exist.
  6. Handles stream errors gracefully.

Serving the Index File

Most web servers need to serve an index.html file when a directory is requested. Here is an enhanced version that handles this:

struct StaticFileMiddleware {
    static_path: String,
}

impl ServerHook for StaticFileMiddleware {
    async fn new(_: &mut Stream, _: &mut Context) -> Self {
        Self {
            static_path: "./static/".to_string(),
        }
    }

    async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
        let path = ctx.get_request().get_path();

        // Resolve the file path, defaulting to index.html for directories
        let file_path = if path.ends_with('/') {
            format!("{static_path}{}index.html", path)
        } else {
            format!("{static_path}{path}")
        };

        let file_extension = FileExtension::get_extension_name(&file_path);
        let content_type = FileExtension::parse(&file_extension).get_content_type();

        match tokio::fs::read_to_string(&file_path).await {
            Ok(file_data) => {
                ctx.get_mut_response()
                    .set_version(HttpVersion::Http1_1)
                    .set_status_code(200)
                    .set_header(CONTENT_TYPE, &content_type)
                    .set_body(file_data);

                let data = ctx.get_mut_response().build();
                if stream.try_send(data).await.is_err() {
                    stream.set_closed(true);
                }
            }
            Err(_) => {
                ctx.get_mut_response()
                    .set_version(HttpVersion::Http1_1)
                    .set_status_code(404)
                    .set_body("File not found");

                let data = ctx.get_mut_response().build();
                if stream.try_send(data).await.is_err() {
                    stream.set_closed(true);
                }
            }
        }

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

Project Directory Structure

Hyperlane recommends the following project directory structure, which includes a static directory for static assets:

├── application/
│   ├── controller/
│   ├── domain/
│   ├── exception/
│   ├── mapper/
│   ├── middleware/
│   ├── model/
│   ├── repository/
│   ├── service/
│   ├── utils/
│   └── view
├── bootstrap/
├── config/
├── plugin/
│   ├── database/
│   ├── env/
│   ├── logger/
│   ├── mysql/
│   ├── postgresql/
│   ├── process/
│   └── redis
└── resources/
    ├── docker/
    ├── env/
    ├── sql/
    ├── static/
    └── templates
Enter fullscreen mode Exit fullscreen mode

The resources/static/ directory is the conventional location for static assets such as HTML, CSS, JavaScript, images, and other files.

Static Files with Route Filters

You can use route filters to control which paths are served static files:

#[host("example.com")]
struct StaticFileRoute;

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

    async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
        let path = ctx.get_request().get_path();
        let static_path = "./static/";
        let file_path = format!("{static_path}{path}");

        let file_extension = FileExtension::get_extension_name(&file_path);
        let content_type = FileExtension::parse(&file_extension).get_content_type();
        let file_data = tokio::fs::read_to_string(&file_path).await.unwrap();

        ctx.get_mut_response().set_body(file_data);

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

        Status::Continue
    }
}

server.route::<StaticFileRoute>("/static/{path}");
Enter fullscreen mode Exit fullscreen mode

Combining Static Files with Other Middleware

Static Files and CORS

When serving static files to cross-origin clients, include CORS headers:

struct StaticCorsMiddleware;

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

    async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
        let path = ctx.get_request().get_path();
        let file_path = format!("./static/{path}");

        let file_extension = FileExtension::get_extension_name(&file_path);
        let content_type = FileExtension::parse(&file_extension).get_content_type();
        let file_data = tokio::fs::read_to_string(&file_path).await.unwrap();

        ctx.get_mut_response()
            .set_header(CONTENT_TYPE, &content_type)
            .set_header(ACCESS_CONTROL_ALLOW_ORIGIN, WILDCARD_ANY)
            .set_body(file_data);

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

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

Static Files and Authentication

You can protect static files behind authentication:

struct ProtectedStaticMiddleware;

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

    async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
        // Check authentication first
        let auth_str = ctx.get_request()
            .try_get_header_back(AUTHORIZATION)
            .unwrap_or_default();

        if auth_str.is_empty() {
            let data = ctx.get_mut_response()
                .set_status_code(401)
                .set_body("Unauthorized")
                .build();

            if stream.try_send(data).await.is_err() {
                stream.set_closed(true);
            }

            return Status::Reject;
        }

        // Serve the static file
        let path = ctx.get_request().get_path();
        let file_path = format!("./static/{path}");

        let file_extension = FileExtension::get_extension_name(&file_path);
        let content_type = FileExtension::parse(&file_extension).get_content_type();
        let file_data = tokio::fs::read_to_string(&file_path).await.unwrap();

        ctx.get_mut_response().set_body(file_data);

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

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

Best Practices for Static File Serving

  1. Always handle file-not-found errors. Use match with tokio::fs::read_to_string to gracefully handle missing files and return appropriate 404 responses.

  2. Set correct content types. Use FileExtension::parse to ensure browsers interpret files correctly.

  3. Use asynchronous file I/O. Always use tokio::fs functions to avoid blocking the async runtime.

  4. Sanitize file paths. Prevent directory traversal attacks by validating that resolved paths stay within the static directory.

  5. Serve index files for directories. When a path ends with /, look for an index.html file in that directory.

  6. Consider caching headers. For production deployments, add Cache-Control headers to reduce server load for frequently accessed files.

  7. Use route filters for access control. Combine static file serving with route filters to restrict access to specific paths or hosts.

  8. Handle stream errors. Always check the result of try_send and close the stream if it fails.

Conclusion

Static file serving in hyperlane is straightforward yet flexible. By combining the FileExtension utility for content type detection with Tokio's asynchronous file I/O and hyperlane's response building API, you can build efficient and secure static file servers. Whether you are serving a simple website or protecting static assets behind authentication, hyperlane provides all the tools you need.

The key components — path resolution, content type detection, file reading, and response delivery — work together seamlessly within hyperlane's middleware system, allowing you to integrate static file serving into your application architecture with minimal overhead.


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

Top comments (0)