Skip to content

StaticRouter

StaticRouter

StaticRouter(serve: str, index_file: str = 'index.html', retry_with_index: bool = False)

Bases: Router

Router for serving static files from a local directory or S3 bucket.

This router provides a simple way to mount a directory of static files to a RestMachine application. It includes security features like path traversal protection and supports serving index files for directories.

Supports both local filesystem and S3: - Local: serve="./public" - S3: serve="s3://bucket-name/optional-prefix/"

Only supports GET requests - all other methods return 405 Method Not Allowed.

Example
from restmachine import RestApplication
from restmachine_web import StaticRouter

app = RestApplication()

# Local filesystem
static_router = StaticRouter(serve="./public")
app.mount("/static", static_router)

# S3 bucket
s3_router = StaticRouter(
    serve="s3://my-bucket/assets/",
    retry_with_index=True
)
app.mount("/assets", s3_router)

Initialize the static file router.

Parameters:

Name Type Description Default
serve str

Path to the directory containing static files Can be local path or S3 URI (s3://bucket-name/prefix/)

required
index_file str

Name of the index file to serve for directory requests (default: "index.html")

'index.html'
retry_with_index bool

If True and initial S3 GetObject fails, retry with index file appended (treats path as directory)

False

Raises:

Type Description
ValueError

If local directory doesn't exist or is not a directory

ImportError

If S3 path is used but boto3 is not installed

Source code in packages/restmachine-web/src/restmachine_web/static_router.py
def __init__(
    self,
    serve: str,
    index_file: str = "index.html",
    retry_with_index: bool = False
):
    """Initialize the static file router.

    Args:
        serve: Path to the directory containing static files
              Can be local path or S3 URI (s3://bucket-name/prefix/)
        index_file: Name of the index file to serve for directory requests
                   (default: "index.html")
        retry_with_index: If True and initial S3 GetObject fails, retry with
                        index file appended (treats path as directory)

    Raises:
        ValueError: If local directory doesn't exist or is not a directory
        ImportError: If S3 path is used but boto3 is not installed
    """
    super().__init__()

    self.index_file = index_file
    self.retry_with_index = retry_with_index

    # Detect if this is an S3 path
    if serve.startswith("s3://"):
        self.is_s3 = True
        self._parse_s3_path(serve)
    else:
        self.is_s3 = False
        self.directory = PathType(serve).resolve()

        if not self.directory.exists():
            raise ValueError(f"Directory does not exist: {serve}")
        if not self.directory.is_dir():
            raise ValueError(f"Path is not a directory: {serve}")

    # Register the wildcard GET route
    self._register_routes()

Functions

Overview

StaticRouter is a specialized router for serving static files from local filesystem or S3 buckets. It inherits from restmachine.Router and provides secure, convenient static file serving with automatic MIME type detection and path traversal protection.

Class Signature

class StaticRouter(Router):
    def __init__(
        self,
        serve: str,
        index_file: str = "index.html",
        retry_with_index: bool = False
    )

Parameters

serve (required)

Path to serve files from. Can be:

  • Local path: "./public", "/var/www/html", "static/"
  • S3 URI: "s3://bucket-name/prefix/"
# Local filesystem
StaticRouter(serve="./public")
StaticRouter(serve="/var/www/static")

# S3 bucket
StaticRouter(serve="s3://my-bucket/assets/")
StaticRouter(serve="s3://website-bucket/")

index_file (optional)

Default: "index.html"

Name of the index file to serve for directory requests.

# Serve index.html for directories
StaticRouter(serve="./public", index_file="index.html")

# Custom index file
StaticRouter(serve="./public", index_file="default.htm")

When a directory path is requested (e.g., /static/docs/), the router will serve index_file from that directory (./public/docs/index.html).

retry_with_index (optional)

Default: False

For S3 paths only. If True and the initial S3 GetObject fails, retry by appending the index file name.

StaticRouter(
    serve="s3://my-bucket/site/",
    retry_with_index=True
)

# GET /about → tries:
#   1. s3://my-bucket/site/about
#   2. s3://my-bucket/site/about/index.html

Useful for static websites where paths like /about should serve /about/index.html.

Attributes

directory (PathType, local only)

Resolved absolute path to the local directory being served.

router = StaticRouter(serve="./public")
print(router.directory)  # /home/user/project/public

is_s3 (bool)

Whether this router serves from S3 or local filesystem.

local = StaticRouter(serve="./public")
print(local.is_s3)  # False

s3 = StaticRouter(serve="s3://bucket/")
print(s3.is_s3)  # True

s3_bucket (str, S3 only)

S3 bucket name.

router = StaticRouter(serve="s3://my-bucket/assets/")
print(router.s3_bucket)  # "my-bucket"

s3_prefix (str, S3 only)

S3 key prefix (path within bucket).

router = StaticRouter(serve="s3://my-bucket/assets/css/")
print(router.s3_prefix)  # "assets/css/"

Methods

_serve_file(path: str) -> Response

Internal method that serves a file from the configured location.

Parameters: - path (str): Requested file path (relative to serve location)

Returns: - Response: RestMachine response with file contents

Behavior: - Normalizes and validates the path - Prevents directory traversal attacks - Detects MIME type from file extension - Returns 404 if file not found - Returns 500 on read errors

HTTP Methods

Allowed: GET

Only GET requests are handled. Static files are read-only.

GET /static/style.css     # ✓ Returns file
HEAD /static/style.css    # ✓ Returns headers only

Not Allowed: POST, PUT, DELETE, PATCH

All modification methods return 405 Method Not Allowed.

POST /static/file.txt     # ✗ 405
PUT /static/file.txt      # ✗ 405
DELETE /static/file.txt   # ✗ 405
PATCH /static/file.txt    # ✗ 405

Response Headers

Content-Type

Automatically detected from file extension:

Extension Content-Type
.html text/html
.css text/css
.js application/javascript
.json application/json
.png image/png
.jpg, .jpeg image/jpeg
.svg image/svg+xml
.pdf application/pdf
.txt text/plain
(unknown) application/octet-stream

Content-Length

Set automatically based on file size.

Security Features

Path Traversal Prevention

Requested paths are normalized and validated to prevent directory traversal:

# These are blocked (return 404):
GET /../../../etc/passwd
GET /..%2F..%2Fetc%2Fpasswd
GET /./../../secret.key

The router ensures all served files are within the configured serve directory.

Path Normalization

Multiple slashes and relative path components are normalized:

# All normalize to: /css/style.css
GET //css//style.css
GET /./css/./style.css
GET /css/../css/style.css

Error Responses

404 Not Found

Returned when: - File doesn't exist - Directory requested without index file - Path traversal detected

# File doesn't exist
GET /static/nonexistent.txt  # 404

# Directory without index.html
GET /static/docs/  # 404 if docs/index.html doesn't exist

# Invalid path
GET /static/../../../etc/passwd  # 404

403 Forbidden

Returned for S3 access denied errors:

# S3 object exists but no permission
GET /static/protected.pdf  # 403 if S3 returns AccessDenied

405 Method Not Allowed

Returned for non-GET requests:

POST /static/file.txt    # 405
DELETE /static/file.txt  # 405

500 Internal Server Error

Returned for: - S3 service errors - File read errors - Other unexpected errors

Usage Examples

Basic Local Files

from restmachine import RestApplication
from restmachine_web import StaticRouter

app = RestApplication()

static = StaticRouter(serve="./public")
app.mount("/static", static)

# Serves:
# GET /static/index.html → ./public/index.html
# GET /static/css/app.css → ./public/css/app.css

S3 with Prefix

static = StaticRouter(serve="s3://my-bucket/assets/v1/")
app.mount("/assets", static)

# Serves:
# GET /assets/logo.png → s3://my-bucket/assets/v1/logo.png
# GET /assets/js/app.js → s3://my-bucket/assets/v1/js/app.js

Multiple Mounts

# Frontend app
frontend = StaticRouter(serve="./frontend/build")
app.mount("/app", frontend)

# Documentation
docs = StaticRouter(serve="./docs/html")
app.mount("/docs", docs)

# User uploads (S3)
uploads = StaticRouter(serve="s3://bucket/uploads/")
app.mount("/uploads", uploads)

Custom Index File

static = StaticRouter(
    serve="./public",
    index_file="home.html"  # Serve home.html instead of index.html
)
app.mount("/", static)

# GET / → ./public/home.html
# GET /about/ → ./public/about/home.html

S3 Static Website

static = StaticRouter(
    serve="s3://my-site/",
    index_file="index.html",
    retry_with_index=True  # Try path + index.html on failure
)
app.mount("/", static)

# GET /about → tries:
#   1. s3://my-site/about (fails)
#   2. s3://my-site/about/index.html (succeeds)

Installation

Local filesystem only:

pip install restmachine-web

With S3 support:

pip install restmachine-web[s3]

This installs boto3 for S3 access.

See Also