CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-starlette

The little ASGI library that shines.

Overview
Eval results
Files

static-files.mddocs/

Static File Serving

Starlette provides efficient static file serving with automatic content type detection, HTTP caching, range request support, and security features for serving CSS, JavaScript, images, and other static assets.

StaticFiles Class

from starlette.staticfiles import StaticFiles
from starlette.responses import Response
from starlette.types import Scope
from typing import List, Optional
from os import PathLike
import os

class StaticFiles:
    """
    ASGI application for serving static files.
    
    Features:
    - Automatic content type detection
    - HTTP caching headers (ETag, Last-Modified) 
    - Range request support for partial content
    - Directory traversal protection
    - Index file serving
    - Package-based file serving
    """
    
    def __init__(
        self,
        directory: str | PathLike[str] | None = None,
        packages: List[str | tuple[str, str]] | None = None,
        html: bool = False,
        check_dir: bool = True,
        follow_symlink: bool = False,
    ) -> None:
        """
        Initialize static file server.
        
        Args:
            directory: Directory path to serve files from
            packages: Python packages to serve files from
            html: Whether to serve .html files for extensionless URLs
            check_dir: Whether to check directory exists at startup
            follow_symlink: Whether to follow symbolic links
        """
        self.directory = directory
        self.packages = packages or []
        self.html = html
        self.check_dir = check_dir
        self.follow_symlink = follow_symlink
        
        if check_dir:
            self.check_config()
    
    def check_config(self) -> None:
        """
        Validate static files configuration.
        
        Raises:
            RuntimeError: If configuration is invalid
        """
    
    def get_directories(
        self,
        directory: str | None = None,
        packages: List[str | tuple[str, str]] | None = None,
    ) -> List[PathLike[str]]:
        """
        Get list of directories to serve files from.
        
        Args:
            directory: Override directory
            packages: Override packages
            
        Returns:
            List of directory paths to search for files
        """
    
    def get_path(self, scope: Scope) -> str:
        """
        Get file path from request scope.
        
        Args:
            scope: ASGI request scope
            
        Returns:
            File path relative to served directories
        """
    
    def lookup_path(self, path: str) -> tuple[str | None, os.stat_result | None]:
        """
        Find file in configured directories.
        
        Args:
            path: Relative file path
            
        Returns:
            Tuple of (full_path, stat_result) or (None, None) if not found
        """
    
    def get_response(self, path: str, scope: Scope) -> Response:
        """
        Create response for file path.
        
        Args:
            path: File path to serve
            scope: ASGI request scope
            
        Returns:
            HTTP response for the file
        """
    
    @staticmethod
    def file_response(
        full_path: str | PathLike[str],
        stat_result: os.stat_result,
        scope: Scope,
        status_code: int = 200,
    ) -> Response:
        """
        Create optimized file response.
        
        Args:
            full_path: Full path to file
            stat_result: File statistics
            scope: ASGI request scope  
            status_code: HTTP status code
            
        Returns:
            FileResponse or NotModifiedResponse
        """
    
    @staticmethod
    def is_not_modified(
        response_headers: Headers,
        request_headers: Headers,
    ) -> bool:
        """
        Check if file has been modified based on HTTP headers.
        
        Args:
            response_headers: Response headers (ETag, Last-Modified)
            request_headers: Request headers (If-None-Match, If-Modified-Since)
            
        Returns:
            True if file has not been modified
        """

class NotModifiedResponse(Response):
    """
    HTTP 304 Not Modified response.
    
    Returned when client's cached version is still current
    based on ETag or Last-Modified headers.
    """
    
    def __init__(self, headers: Mapping[str, str] | None = None) -> None:
        super().__init__(status_code=304, headers=headers)

Basic Static File Serving

Simple Directory Serving

from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.staticfiles import StaticFiles

# Serve files from directory
app = Starlette(routes=[
    Mount("/static", StaticFiles(directory="static"), name="static"),
])

# Directory structure:
# static/
#   ├── css/
#   │   └── style.css
#   ├── js/
#   │   └── app.js
#   └── images/
#       └── logo.png

# URLs will be:
# /static/css/style.css -> static/css/style.css
# /static/js/app.js -> static/js/app.js
# /static/images/logo.png -> static/images/logo.png

Multiple Static Directories

from starlette.routing import Mount

app = Starlette(routes=[
    # Different mount points for different asset types
    Mount("/css", StaticFiles(directory="assets/css"), name="css"),
    Mount("/js", StaticFiles(directory="assets/js"), name="js"),
    Mount("/images", StaticFiles(directory="assets/images"), name="images"),
    
    # General static files
    Mount("/static", StaticFiles(directory="static"), name="static"),
])

Package-based Static Files

# Serve files from Python package
app = Starlette(routes=[
    Mount("/assets", StaticFiles(packages=["mypackage"]), name="assets"),
])

# Serve from multiple packages
app = Starlette(routes=[
    Mount("/vendor", StaticFiles(packages=[
        "bootstrap",
        "jquery", 
        ("mylib", "static"),  # Serve mylib/static/ directory
    ]), name="vendor"),
])

# Package structure:
# mypackage/
#   ├── __init__.py
#   ├── static/
#   │   ├── style.css
#   │   └── script.js

# URLs will be:
# /assets/style.css -> mypackage/static/style.css
# /assets/script.js -> mypackage/static/script.js

Advanced Configuration

HTML File Serving

# Enable HTML mode for SPA (Single Page Application) support
app = Starlette(routes=[
    Mount("/", StaticFiles(directory="dist", html=True), name="spa"),
])

# With html=True:
# /about -> looks for dist/about.html or dist/about/index.html
# / -> looks for dist/index.html
# /dashboard/users -> looks for dist/dashboard/users.html

# Without html=True (default):
# /about.html -> dist/about.html (exact match only)

Symbolic Link Handling

# Follow symbolic links (security consideration)
app = Starlette(routes=[
    Mount("/static", StaticFiles(
        directory="static",
        follow_symlink=True  # Default: False for security
    ), name="static"),
])

# Security note: Only enable follow_symlink if you trust
# all symbolic links in the directory tree

Directory Validation

# Disable directory check at startup (for dynamic directories)
app = Starlette(routes=[
    Mount("/uploads", StaticFiles(
        directory="user_uploads",
        check_dir=False  # Don't validate directory exists at startup
    ), name="uploads"),
])

# Useful when directory might be created after application starts

URL Generation

Generating Static File URLs

from starlette.routing import Route, Mount
from starlette.responses import HTMLResponse

async def homepage(request):
    # Generate URLs for static files
    css_url = request.url_for("static", path="css/style.css")
    js_url = request.url_for("static", path="js/app.js")
    image_url = request.url_for("static", path="images/logo.png")
    
    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <link rel="stylesheet" href="{css_url}">
    </head>
    <body>
        <img src="{image_url}" alt="Logo">
        <script src="{js_url}"></script>
    </body>
    </html>
    """
    return HTMLResponse(html)

app = Starlette(routes=[
    Route("/", homepage),
    Mount("/static", StaticFiles(directory="static"), name="static"),
])

Template Integration

from starlette.templating import Jinja2Templates

templates = Jinja2Templates(directory="templates")

async def template_page(request):
    return templates.TemplateResponse("page.html", {
        "request": request,
        "title": "My Page"
    })

# In templates/page.html:
# <link rel="stylesheet" href="{{ url_for('static', path='css/style.css') }}">
# <script src="{{ url_for('static', path='js/app.js') }}"></script>

Caching and Performance

HTTP Caching Headers

Static files automatically include caching headers:

# Automatic headers for static files:
# - ETag: Based on file modification time and size
# - Last-Modified: File modification timestamp  
# - Content-Length: File size
# - Content-Type: Detected from file extension

# Client requests with caching headers:
# If-None-Match: "etag-value"
# If-Modified-Since: "timestamp"

# Server responds with:
# 304 Not Modified (if unchanged)
# 200 OK with file content (if changed)

Cache Control

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

class CacheControlMiddleware(BaseHTTPMiddleware):
    """Add Cache-Control headers to static files."""
    
    def __init__(self, app, max_age: int = 31536000):  # 1 year
        super().__init__(app)
        self.max_age = max_age
    
    async def dispatch(self, request, call_next):
        response = await call_next(request)
        
        # Add cache headers for static files
        if request.url.path.startswith("/static/"):
            response.headers["Cache-Control"] = f"public, max-age={self.max_age}"
        
        return response

# Apply to application
app.add_middleware(CacheControlMiddleware, max_age=86400)  # 1 day

Content Compression

from starlette.middleware.gzip import GZipMiddleware

# Add compression for static files
app.add_middleware(GZipMiddleware, minimum_size=500)

# Compresses CSS, JS, HTML, and other text files
# Binary files (images, videos) are not compressed

Security Considerations

Path Traversal Protection

# StaticFiles automatically prevents directory traversal
# These requests are automatically rejected:
# /static/../../../etc/passwd
# /static/..%2F..%2Fetc%2Fpasswd  
# /static/....//....//etc/passwd

# Safe paths only:
# /static/css/style.css ✓
# /static/subdir/file.js ✓

Content Type Validation

import mimetypes
from starlette.staticfiles import StaticFiles
from starlette.responses import Response

class SecureStaticFiles(StaticFiles):
    """StaticFiles with content type restrictions."""
    
    ALLOWED_TYPES = {
        'text/css',
        'text/javascript', 
        'application/javascript',
        'image/png',
        'image/jpeg',
        'image/gif',
        'image/svg+xml',
        'font/woff',
        'font/woff2',
        'application/font-woff',
        'application/font-woff2',
    }
    
    def get_response(self, path: str, scope) -> Response:
        # Get the normal response
        response = super().get_response(path, scope)
        
        # Check content type
        content_type = response.media_type
        if content_type not in self.ALLOWED_TYPES:
            # Return 403 for disallowed file types
            return Response("Forbidden file type", status_code=403)
        
        return response

# Use secure static files
app = Starlette(routes=[
    Mount("/static", SecureStaticFiles(directory="static"), name="static"),
])

File Size Limits

class LimitedStaticFiles(StaticFiles):
    """StaticFiles with size limits."""
    
    def __init__(self, *args, max_size: int = 10 * 1024 * 1024, **kwargs):
        super().__init__(*args, **kwargs)
        self.max_size = max_size  # 10MB default
    
    def get_response(self, path: str, scope) -> Response:
        full_path, stat_result = self.lookup_path(path)
        
        if stat_result and stat_result.st_size > self.max_size:
            return Response("File too large", status_code=413)
        
        return super().get_response(path, scope)

Custom Static File Handlers

Custom File Processing

import os
from starlette.staticfiles import StaticFiles
from starlette.responses import Response

class ProcessedStaticFiles(StaticFiles):
    """StaticFiles with custom processing."""
    
    def get_response(self, path: str, scope) -> Response:
        # Custom processing for CSS files
        if path.endswith('.css'):
            return self.process_css_file(path, scope)
        
        # Custom processing for JS files  
        if path.endswith('.js'):
            return self.process_js_file(path, scope)
        
        # Default handling for other files
        return super().get_response(path, scope)
    
    def process_css_file(self, path: str, scope) -> Response:
        """Process CSS files (minification, variable substitution, etc.)"""
        full_path, stat_result = self.lookup_path(path)
        
        if not full_path:
            return Response("Not found", status_code=404)
        
        # Read and process CSS
        with open(full_path, 'r') as f:
            content = f.read()
        
        # Custom processing (minification, variable replacement, etc.)
        processed_content = self.minify_css(content)
        
        return Response(
            processed_content,
            media_type="text/css",
            headers={
                "Last-Modified": format_date_time(stat_result.st_mtime),
                "ETag": f'"{stat_result.st_mtime}-{stat_result.st_size}"',
            }
        )
    
    def minify_css(self, content: str) -> str:
        """Basic CSS minification."""
        # Remove comments
        import re
        content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
        # Remove extra whitespace
        content = re.sub(r'\s+', ' ', content)
        return content.strip()

Dynamic File Generation

import json
from datetime import datetime
from starlette.responses import JSONResponse

class DynamicStaticFiles(StaticFiles):
    """Serve some files dynamically."""
    
    def get_response(self, path: str, scope) -> Response:
        # Generate manifest.json dynamically
        if path == 'manifest.json':
            return self.generate_manifest(scope)
        
        # Generate sitemap.xml dynamically
        if path == 'sitemap.xml':
            return self.generate_sitemap(scope)
        
        return super().get_response(path, scope)
    
    def generate_manifest(self, scope) -> Response:
        """Generate web app manifest."""
        manifest = {
            "name": "My Web App",
            "short_name": "MyApp",
            "start_url": "/",
            "display": "standalone",
            "background_color": "#ffffff",
            "theme_color": "#000000",
            "generated_at": datetime.now().isoformat(),
        }
        
        return JSONResponse(manifest, headers={
            "Cache-Control": "no-cache"  # Don't cache dynamic content
        })
    
    def generate_sitemap(self, scope) -> Response:
        """Generate XML sitemap."""
        base_url = f"{scope['scheme']}://{scope['server'][0]}"
        
        sitemap = f"""<?xml version="1.0" encoding="UTF-8"?>
        <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
            <url>
                <loc>{base_url}/</loc>
                <lastmod>{datetime.now().date()}</lastmod>
            </url>
        </urlset>"""
        
        return Response(
            sitemap,
            media_type="application/xml",
            headers={"Cache-Control": "no-cache"}
        )

Testing Static Files

Testing Static File Serving

from starlette.testclient import TestClient
import os

def test_static_files():
    # Create test file
    os.makedirs("test_static", exist_ok=True)
    with open("test_static/test.css", "w") as f:
        f.write("body { color: red; }")
    
    app = Starlette(routes=[
        Mount("/static", StaticFiles(directory="test_static"), name="static"),
    ])
    
    client = TestClient(app)
    
    # Test file serving
    response = client.get("/static/test.css")
    assert response.status_code == 200
    assert response.text == "body { color: red; }"
    assert response.headers["content-type"] == "text/css"
    
    # Test 404 for missing file
    response = client.get("/static/missing.css")
    assert response.status_code == 404
    
    # Test caching headers
    response = client.get("/static/test.css")
    assert "etag" in response.headers
    assert "last-modified" in response.headers
    
    # Test conditional request
    etag = response.headers["etag"]
    response = client.get("/static/test.css", headers={
        "If-None-Match": etag
    })
    assert response.status_code == 304

def test_url_generation():
    app = Starlette(routes=[
        Route("/", lambda r: JSONResponse({"css_url": str(r.url_for("static", path="style.css"))})),
        Mount("/static", StaticFiles(directory="static"), name="static"),
    ])
    
    client = TestClient(app)
    response = client.get("/")
    assert response.json()["css_url"] == "http://testserver/static/style.css"

Performance Testing

import time
import statistics

def test_static_file_performance():
    app = Starlette(routes=[
        Mount("/static", StaticFiles(directory="static"), name="static"),
    ])
    
    client = TestClient(app)
    
    # Measure response times
    times = []
    for _ in range(100):
        start = time.time()
        response = client.get("/static/large_file.js")
        end = time.time()
        
        assert response.status_code == 200
        times.append(end - start)
    
    # Check performance metrics
    avg_time = statistics.mean(times)
    max_time = max(times)
    
    assert avg_time < 0.1  # Average under 100ms
    assert max_time < 0.5  # Max under 500ms

Static file serving in Starlette provides efficient, secure, and flexible asset delivery with automatic optimization, caching, and security features suitable for production applications.

Install with Tessl CLI

npx tessl i tessl/pypi-starlette

docs

authentication.md

core-application.md

data-structures.md

exceptions-status.md

index.md

middleware.md

requests-responses.md

routing.md

static-files.md

testing.md

websockets.md

tile.json