CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-sphinx-autobuild

Rebuild Sphinx documentation on changes, with hot reloading in the browser.

Overview
Eval results
Files

middleware.mddocs/

HTTP Middleware

ASGI middleware for injecting WebSocket client code into HTML responses to enable hot reloading functionality. The middleware automatically adds JavaScript code to HTML pages that establishes a WebSocket connection for real-time browser refresh notifications.

Capabilities

JavascriptInjectorMiddleware Class

ASGI middleware that injects WebSocket client JavaScript into HTML responses.

class JavascriptInjectorMiddleware:
    def __init__(self, app: ASGIApp, ws_url: str) -> None:
        """
        Initialize middleware with ASGI app and WebSocket URL.
        
        Parameters:
        - app: ASGIApp - The ASGI application to wrap
        - ws_url: str - WebSocket URL in format "host:port" for client connections
        
        Processing:
        - Pre-generates WebSocket client script using web_socket_script()
        - Encodes script to UTF-8 bytes for efficient injection
        - Stores references for use in request handling
        """
    
    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        """
        ASGI middleware callable for HTTP requests.
        
        Automatically injects WebSocket JavaScript into HTML responses:
        - Only processes HTTP requests (ignores WebSocket, etc.)
        - Detects HTML responses by Content-Type header
        - Adjusts Content-Length header when script is injected
        - Appends script to response body on final chunk
        
        Parameters:
        - scope: Scope - ASGI request scope with connection info
        - receive: Receive - ASGI receive callable for request messages  
        - send: Send - ASGI send callable for response messages
        
        Returns:
        - None
        
        Behavior:
        - Non-HTTP requests: Pass through unchanged
        - Non-HTML responses: Pass through unchanged  
        - HTML responses: Inject WebSocket script at end of body
        - Streaming responses: Only inject on final body chunk
        """

WebSocket Script Generation

Generate the JavaScript code that browsers execute to establish WebSocket connections.

def web_socket_script(ws_url: str) -> str:
    """
    Generate WebSocket JavaScript code for browser auto-reload.
    
    Creates a complete HTML script block with WebSocket client code that:
    - Establishes WebSocket connection to specified URL
    - Listens for messages from server
    - Triggers browser reload on any message received
    
    Parameters:
    - ws_url: str - WebSocket URL in format "host:port"
    
    Returns:
    - str - Complete HTML script element with WebSocket client code
    
    Generated Script Behavior:
    - Creates WebSocket connection to "ws://{ws_url}/websocket-reload"
    - Automatically reloads page when server sends any message
    - Handles connection errors gracefully (no error handling shown to user)
    """

Usage Examples

Basic Middleware Setup

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.staticfiles import StaticFiles
from sphinx_autobuild.middleware import JavascriptInjectorMiddleware

# Create ASGI application with middleware
app = Starlette(
    middleware=[
        Middleware(JavascriptInjectorMiddleware, ws_url="127.0.0.1:8000")
    ]
)

Complete Server Integration

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount, WebSocketRoute
from starlette.staticfiles import StaticFiles
from sphinx_autobuild.middleware import JavascriptInjectorMiddleware
from sphinx_autobuild.server import RebuildServer

# Setup components
server = RebuildServer(watch_dirs, ignore_filter, builder)
url_host = "127.0.0.1:8000"

# Create app with middleware and routes
app = Starlette(
    routes=[
        WebSocketRoute("/websocket-reload", server, name="reload"),
        Mount("/", app=StaticFiles(directory="_build/html", html=True), name="static"),
    ],
    middleware=[
        Middleware(JavascriptInjectorMiddleware, ws_url=url_host)
    ],
    lifespan=server.lifespan,
)

Custom Script Generation

from sphinx_autobuild.middleware import web_socket_script

# Generate script for different URLs
local_script = web_socket_script("127.0.0.1:8000")
network_script = web_socket_script("192.168.1.100:8080")

print(local_script)
# Output:
# <script>
# const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");
# ws.onmessage = () => window.location.reload();
# </script>

# Use in custom middleware or manual injection
custom_html = f"""
<!DOCTYPE html>
<html>
<head><title>My Docs</title></head>
<body>
    <h1>Documentation</h1>
    {network_script}
</body>
</html>
"""

How Injection Works

HTTP Response Processing

The middleware intercepts HTTP responses and modifies them:

  1. Request Filtering: Only processes HTTP requests (not WebSocket)
  2. Content-Type Detection: Checks for "text/html" Content-Type header
  3. Header Adjustment: Updates Content-Length to account for injected script
  4. Body Modification: Appends WebSocket script to final response body chunk
  5. Pass-through: Non-HTML responses are unmodified

Script Injection Details

# The generated script is minimal and efficient:
script_template = """
<script>
const ws = new WebSocket("ws://{ws_url}/websocket-reload");
ws.onmessage = () => window.location.reload();
</script>
"""

# Key characteristics:
# - No error handling (fails silently if WebSocket unavailable)
# - Automatic reconnection on page reload
# - Triggers on any message (server sends "refresh")
# - Minimal performance impact

Content-Length Handling

The middleware properly handles HTTP Content-Length headers:

# When HTML is detected:
if "Content-Length" in headers:
    original_length = int(headers["Content-Length"])
    new_length = original_length + len(script_bytes)
    headers["Content-Length"] = str(new_length)

# This ensures:
# - HTTP/1.1 clients receive correct content length
# - Proxy servers handle responses correctly
# - No truncation of response body

Browser Compatibility

WebSocket Support

The injected JavaScript requires WebSocket support:

  • Modern Browsers: Full support (Chrome, Firefox, Safari, Edge)
  • Legacy Browsers: IE 10+, older mobile browsers may need polyfills
  • Fallback: Pages still function without WebSocket (no auto-reload)

Connection Behavior

// Browser-side connection handling:
const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");

// Automatic behaviors:
ws.onopen = () => {
    // Connection established - ready for reload messages
};

ws.onmessage = () => {
    window.location.reload();  // Refresh entire page
};

ws.onclose = () => {
    // Connection lost - will reconnect on next page load
};

ws.onerror = () => {
    // Connection error - fails silently, no user notification
};

Security Considerations

Cross-Origin Policy

WebSocket connections respect browser security policies:

  • Same-origin: Works automatically for same-origin requests
  • Cross-origin: May require CORS configuration for different hosts
  • Mixed content: HTTPS pages cannot connect to WS (non-secure WebSocket)

Development vs Production

This middleware is designed for development only:

  • Performance: Adds overhead to every HTML response
  • Security: Injects arbitrary JavaScript into responses
  • Functionality: Hot reloading is not needed in production

Production Deployment: Remove middleware from production deployments:

# Development configuration
if DEBUG:
    middleware.append(Middleware(JavascriptInjectorMiddleware, ws_url=url_host))

# Or use environment-based configuration
import os
if os.getenv("SPHINX_AUTOBUILD_DEV"):
    middleware.append(Middleware(JavascriptInjectorMiddleware, ws_url=url_host))

Performance Characteristics

Injection Overhead

  • CPU: Minimal - simple string concatenation on final body chunk
  • Memory: Low - pre-generated script cached in middleware instance
  • Network: Small - typically 100-200 bytes added to each HTML response
  • Latency: Negligible - injection happens during response streaming

Selective Processing

Middleware optimizes by only processing relevant requests:

  • HTTP Only: Ignores WebSocket, SSE, and other protocol requests
  • HTML Only: Skips CSS, JavaScript, images, and other content types
  • Final Chunk: Only modifies the last body chunk in streaming responses
  • Header Check: Early Content-Type detection avoids unnecessary processing

Install with Tessl CLI

npx tessl i tessl/pypi-sphinx-autobuild

docs

build.md

cli.md

filtering.md

index.md

middleware.md

server.md

utils.md

tile.json