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:
- Path resolution — Mapping URL paths to filesystem paths.
- Content type detection — Determining the correct MIME type for each file.
- File reading — Reading file contents from disk.
- 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);
This code:
- Constructs the full filesystem file_path by combining a base directory with the request path.
- Extracts the file extension using
FileExtension::get_extension_name. - Looks up the MIME content type using
FileExtension::parse. - Reads the file contents asynchronously using Tokio's
read_to_string. - 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:
-
.html→text/html -
.css→text/css -
.js→application/javascript -
.json→application/json -
.png→image/png -
.jpg/.jpeg→image/jpeg -
.gif→image/gif -
.svg→image/svg+xml -
.ico→image/x-icon -
.txt→text/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>();
This middleware:
- Resolves the URL path to a filesystem path.
- Determines the content type from the file extension.
- Reads the file asynchronously.
- Returns a 200 response with the file content on success.
- Returns a 404 response if the file does not exist.
- 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
}
}
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
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}");
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
}
}
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
}
}
Best Practices for Static File Serving
Always handle file-not-found errors. Use
matchwithtokio::fs::read_to_stringto gracefully handle missing files and return appropriate 404 responses.Set correct content types. Use
FileExtension::parseto ensure browsers interpret files correctly.Use asynchronous file I/O. Always use
tokio::fsfunctions to avoid blocking the async runtime.Sanitize file paths. Prevent directory traversal attacks by validating that resolved paths stay within the static directory.
Serve index files for directories. When a path ends with
/, look for anindex.htmlfile in that directory.Consider caching headers. For production deployments, add
Cache-Controlheaders to reduce server load for frequently accessed files.Use route filters for access control. Combine static file serving with route filters to restrict access to specific paths or hosts.
Handle stream errors. Always check the result of
try_sendand 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)