0
# HTTP Middleware
1
2
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.
3
4
## Capabilities
5
6
### JavascriptInjectorMiddleware Class
7
8
ASGI middleware that injects WebSocket client JavaScript into HTML responses.
9
10
```python { .api }
11
class JavascriptInjectorMiddleware:
12
def __init__(self, app: ASGIApp, ws_url: str) -> None:
13
"""
14
Initialize middleware with ASGI app and WebSocket URL.
15
16
Parameters:
17
- app: ASGIApp - The ASGI application to wrap
18
- ws_url: str - WebSocket URL in format "host:port" for client connections
19
20
Processing:
21
- Pre-generates WebSocket client script using web_socket_script()
22
- Encodes script to UTF-8 bytes for efficient injection
23
- Stores references for use in request handling
24
"""
25
26
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
27
"""
28
ASGI middleware callable for HTTP requests.
29
30
Automatically injects WebSocket JavaScript into HTML responses:
31
- Only processes HTTP requests (ignores WebSocket, etc.)
32
- Detects HTML responses by Content-Type header
33
- Adjusts Content-Length header when script is injected
34
- Appends script to response body on final chunk
35
36
Parameters:
37
- scope: Scope - ASGI request scope with connection info
38
- receive: Receive - ASGI receive callable for request messages
39
- send: Send - ASGI send callable for response messages
40
41
Returns:
42
- None
43
44
Behavior:
45
- Non-HTTP requests: Pass through unchanged
46
- Non-HTML responses: Pass through unchanged
47
- HTML responses: Inject WebSocket script at end of body
48
- Streaming responses: Only inject on final body chunk
49
"""
50
```
51
52
### WebSocket Script Generation
53
54
Generate the JavaScript code that browsers execute to establish WebSocket connections.
55
56
```python { .api }
57
def web_socket_script(ws_url: str) -> str:
58
"""
59
Generate WebSocket JavaScript code for browser auto-reload.
60
61
Creates a complete HTML script block with WebSocket client code that:
62
- Establishes WebSocket connection to specified URL
63
- Listens for messages from server
64
- Triggers browser reload on any message received
65
66
Parameters:
67
- ws_url: str - WebSocket URL in format "host:port"
68
69
Returns:
70
- str - Complete HTML script element with WebSocket client code
71
72
Generated Script Behavior:
73
- Creates WebSocket connection to "ws://{ws_url}/websocket-reload"
74
- Automatically reloads page when server sends any message
75
- Handles connection errors gracefully (no error handling shown to user)
76
"""
77
```
78
79
## Usage Examples
80
81
### Basic Middleware Setup
82
83
```python
84
from starlette.applications import Starlette
85
from starlette.middleware import Middleware
86
from starlette.staticfiles import StaticFiles
87
from sphinx_autobuild.middleware import JavascriptInjectorMiddleware
88
89
# Create ASGI application with middleware
90
app = Starlette(
91
middleware=[
92
Middleware(JavascriptInjectorMiddleware, ws_url="127.0.0.1:8000")
93
]
94
)
95
```
96
97
### Complete Server Integration
98
99
```python
100
from starlette.applications import Starlette
101
from starlette.middleware import Middleware
102
from starlette.routing import Mount, WebSocketRoute
103
from starlette.staticfiles import StaticFiles
104
from sphinx_autobuild.middleware import JavascriptInjectorMiddleware
105
from sphinx_autobuild.server import RebuildServer
106
107
# Setup components
108
server = RebuildServer(watch_dirs, ignore_filter, builder)
109
url_host = "127.0.0.1:8000"
110
111
# Create app with middleware and routes
112
app = Starlette(
113
routes=[
114
WebSocketRoute("/websocket-reload", server, name="reload"),
115
Mount("/", app=StaticFiles(directory="_build/html", html=True), name="static"),
116
],
117
middleware=[
118
Middleware(JavascriptInjectorMiddleware, ws_url=url_host)
119
],
120
lifespan=server.lifespan,
121
)
122
```
123
124
### Custom Script Generation
125
126
```python
127
from sphinx_autobuild.middleware import web_socket_script
128
129
# Generate script for different URLs
130
local_script = web_socket_script("127.0.0.1:8000")
131
network_script = web_socket_script("192.168.1.100:8080")
132
133
print(local_script)
134
# Output:
135
# <script>
136
# const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");
137
# ws.onmessage = () => window.location.reload();
138
# </script>
139
140
# Use in custom middleware or manual injection
141
custom_html = f"""
142
<!DOCTYPE html>
143
<html>
144
<head><title>My Docs</title></head>
145
<body>
146
<h1>Documentation</h1>
147
{network_script}
148
</body>
149
</html>
150
"""
151
```
152
153
## How Injection Works
154
155
### HTTP Response Processing
156
157
The middleware intercepts HTTP responses and modifies them:
158
159
1. **Request Filtering**: Only processes HTTP requests (not WebSocket)
160
2. **Content-Type Detection**: Checks for "text/html" Content-Type header
161
3. **Header Adjustment**: Updates Content-Length to account for injected script
162
4. **Body Modification**: Appends WebSocket script to final response body chunk
163
5. **Pass-through**: Non-HTML responses are unmodified
164
165
### Script Injection Details
166
167
```python
168
# The generated script is minimal and efficient:
169
script_template = """
170
<script>
171
const ws = new WebSocket("ws://{ws_url}/websocket-reload");
172
ws.onmessage = () => window.location.reload();
173
</script>
174
"""
175
176
# Key characteristics:
177
# - No error handling (fails silently if WebSocket unavailable)
178
# - Automatic reconnection on page reload
179
# - Triggers on any message (server sends "refresh")
180
# - Minimal performance impact
181
```
182
183
### Content-Length Handling
184
185
The middleware properly handles HTTP Content-Length headers:
186
187
```python
188
# When HTML is detected:
189
if "Content-Length" in headers:
190
original_length = int(headers["Content-Length"])
191
new_length = original_length + len(script_bytes)
192
headers["Content-Length"] = str(new_length)
193
194
# This ensures:
195
# - HTTP/1.1 clients receive correct content length
196
# - Proxy servers handle responses correctly
197
# - No truncation of response body
198
```
199
200
## Browser Compatibility
201
202
### WebSocket Support
203
204
The injected JavaScript requires WebSocket support:
205
- **Modern Browsers**: Full support (Chrome, Firefox, Safari, Edge)
206
- **Legacy Browsers**: IE 10+, older mobile browsers may need polyfills
207
- **Fallback**: Pages still function without WebSocket (no auto-reload)
208
209
### Connection Behavior
210
211
```javascript
212
// Browser-side connection handling:
213
const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");
214
215
// Automatic behaviors:
216
ws.onopen = () => {
217
// Connection established - ready for reload messages
218
};
219
220
ws.onmessage = () => {
221
window.location.reload(); // Refresh entire page
222
};
223
224
ws.onclose = () => {
225
// Connection lost - will reconnect on next page load
226
};
227
228
ws.onerror = () => {
229
// Connection error - fails silently, no user notification
230
};
231
```
232
233
## Security Considerations
234
235
### Cross-Origin Policy
236
237
WebSocket connections respect browser security policies:
238
- **Same-origin**: Works automatically for same-origin requests
239
- **Cross-origin**: May require CORS configuration for different hosts
240
- **Mixed content**: HTTPS pages cannot connect to WS (non-secure WebSocket)
241
242
### Development vs Production
243
244
This middleware is designed for **development only**:
245
- **Performance**: Adds overhead to every HTML response
246
- **Security**: Injects arbitrary JavaScript into responses
247
- **Functionality**: Hot reloading is not needed in production
248
249
**Production Deployment**: Remove middleware from production deployments:
250
251
```python
252
# Development configuration
253
if DEBUG:
254
middleware.append(Middleware(JavascriptInjectorMiddleware, ws_url=url_host))
255
256
# Or use environment-based configuration
257
import os
258
if os.getenv("SPHINX_AUTOBUILD_DEV"):
259
middleware.append(Middleware(JavascriptInjectorMiddleware, ws_url=url_host))
260
```
261
262
## Performance Characteristics
263
264
### Injection Overhead
265
266
- **CPU**: Minimal - simple string concatenation on final body chunk
267
- **Memory**: Low - pre-generated script cached in middleware instance
268
- **Network**: Small - typically 100-200 bytes added to each HTML response
269
- **Latency**: Negligible - injection happens during response streaming
270
271
### Selective Processing
272
273
Middleware optimizes by only processing relevant requests:
274
- **HTTP Only**: Ignores WebSocket, SSE, and other protocol requests
275
- **HTML Only**: Skips CSS, JavaScript, images, and other content types
276
- **Final Chunk**: Only modifies the last body chunk in streaming responses
277
- **Header Check**: Early Content-Type detection avoids unnecessary processing