0
# Handlers and Utilities
1
2
The Jupyter Server provides base handler classes and utilities for building secure, authenticated web handlers and APIs.
3
4
## Base Handler Classes
5
6
### JupyterHandler
7
8
Core base handler for all Jupyter server requests with authentication and common functionality.
9
10
```python
11
from jupyter_server.base.handlers import JupyterHandler
12
```
13
14
```python{ .api }
15
from jupyter_server.base.handlers import JupyterHandler
16
from jupyter_server.auth.decorator import authorized
17
from tornado import web
18
19
class MyCustomHandler(JupyterHandler):
20
"""Custom handler extending JupyterHandler."""
21
22
@authorized
23
async def get(self):
24
"""Handle GET request."""
25
# Access current authenticated user
26
user = self.current_user
27
if user:
28
self.finish({
29
"message": f"Hello {user.display_name}!",
30
"username": user.username,
31
})
32
else:
33
raise web.HTTPError(401, "Authentication required")
34
35
@authorized
36
async def post(self):
37
"""Handle POST request."""
38
# Get JSON request body
39
try:
40
data = self.get_json_body()
41
except ValueError:
42
raise web.HTTPError(400, "Invalid JSON")
43
44
# Process data
45
result = await self.process_data(data)
46
47
# Return JSON response
48
self.finish({"result": result})
49
50
async def process_data(self, data):
51
"""Process request data."""
52
# Implement custom logic
53
return f"Processed: {data}"
54
55
def write_error(self, status_code, **kwargs):
56
"""Custom error handling."""
57
self.set_header("Content-Type", "application/json")
58
error_message = {
59
"error": {
60
"code": status_code,
61
"message": self._reason,
62
}
63
}
64
self.finish(error_message)
65
```
66
67
### APIHandler
68
69
Base class for REST API endpoints with JSON handling and CORS support.
70
71
```python{ .api }
72
from jupyter_server.base.handlers import APIHandler
73
from jupyter_server.auth.decorator import authorized
74
from tornado import web
75
import json
76
77
class MyAPIHandler(APIHandler):
78
"""REST API handler."""
79
80
@authorized
81
async def get(self, resource_id=None):
82
"""Get resource(s)."""
83
if resource_id:
84
# Get specific resource
85
resource = await self.get_resource(resource_id)
86
if not resource:
87
raise web.HTTPError(404, f"Resource {resource_id} not found")
88
self.finish(resource)
89
else:
90
# List all resources
91
resources = await self.list_resources()
92
self.finish({"resources": resources})
93
94
@authorized
95
async def post(self):
96
"""Create new resource."""
97
data = self.get_json_body()
98
99
# Validate required fields
100
required_fields = ["name", "type"]
101
for field in required_fields:
102
if field not in data:
103
raise web.HTTPError(400, f"Missing required field: {field}")
104
105
# Create resource
106
resource = await self.create_resource(data)
107
self.set_status(201) # Created
108
self.finish(resource)
109
110
@authorized
111
async def put(self, resource_id):
112
"""Update existing resource."""
113
data = self.get_json_body()
114
115
# Check if resource exists
116
existing = await self.get_resource(resource_id)
117
if not existing:
118
raise web.HTTPError(404, f"Resource {resource_id} not found")
119
120
# Update resource
121
updated = await self.update_resource(resource_id, data)
122
self.finish(updated)
123
124
@authorized
125
async def delete(self, resource_id):
126
"""Delete resource."""
127
# Check if resource exists
128
existing = await self.get_resource(resource_id)
129
if not existing:
130
raise web.HTTPError(404, f"Resource {resource_id} not found")
131
132
# Delete resource
133
await self.delete_resource(resource_id)
134
self.set_status(204) # No Content
135
self.finish()
136
137
async def get_resource(self, resource_id):
138
"""Get resource by ID."""
139
# Implement resource retrieval
140
return {"id": resource_id, "name": "Example Resource"}
141
142
async def list_resources(self):
143
"""List all resources."""
144
# Implement resource listing
145
return [
146
{"id": "1", "name": "Resource 1"},
147
{"id": "2", "name": "Resource 2"},
148
]
149
150
async def create_resource(self, data):
151
"""Create new resource."""
152
# Implement resource creation
153
resource_id = "new-id"
154
return {"id": resource_id, **data}
155
156
async def update_resource(self, resource_id, data):
157
"""Update existing resource."""
158
# Implement resource update
159
return {"id": resource_id, **data}
160
161
async def delete_resource(self, resource_id):
162
"""Delete resource."""
163
# Implement resource deletion
164
pass
165
```
166
167
### AuthenticatedHandler
168
169
Base for handlers requiring authentication.
170
171
```python{ .api }
172
from jupyter_server.base.handlers import AuthenticatedHandler
173
from tornado import web
174
175
class MyAuthenticatedHandler(AuthenticatedHandler):
176
"""Handler requiring authentication."""
177
178
async def get(self):
179
"""GET requires authentication."""
180
# User is guaranteed to be authenticated
181
user = self.current_user
182
from dataclasses import asdict
183
self.finish({
184
"authenticated": True,
185
"user": asdict(user),
186
"message": "This is a protected endpoint",
187
})
188
189
async def prepare(self):
190
"""Called before any HTTP method."""
191
await super().prepare()
192
193
# Custom preparation logic
194
self.request_start_time = time.time()
195
196
def on_finish(self):
197
"""Called after request completion."""
198
if hasattr(self, 'request_start_time'):
199
duration = time.time() - self.request_start_time
200
self.log.info(f"Request took {duration:.3f} seconds")
201
```
202
203
## Static File Handling
204
205
### AuthenticatedFileHandler
206
207
Serves static files with authentication requirements.
208
209
```python{ .api }
210
from jupyter_server.base.handlers import AuthenticatedFileHandler
211
import os
212
213
# Configure authenticated static file serving
214
static_path = "/path/to/static/files"
215
handler_class = AuthenticatedFileHandler
216
handler_settings = {"path": static_path}
217
218
# In web application setup
219
handlers = [
220
(r"/static/(.*)", handler_class, handler_settings),
221
]
222
223
# Usage: Files at /path/to/static/files/* are served at /static/*
224
# Authentication is required to access these files
225
```
226
227
### FileFindHandler
228
229
Advanced file discovery and serving with caching.
230
231
```python{ .api }
232
from jupyter_server.base.handlers import FileFindHandler
233
234
# Configure file finder
235
file_finder_settings = {
236
"path": ["/path/to/files", "/another/path"],
237
"default_filename": "index.html",
238
}
239
240
handlers = [
241
(r"/files/(.*)", FileFindHandler, file_finder_settings),
242
]
243
244
# Features:
245
# - Searches multiple paths in order
246
# - Caches file locations for performance
247
# - Supports default filenames for directories
248
# - Content type detection
249
# - ETags and caching headers
250
```
251
252
## WebSocket Handlers
253
254
### ZMQStreamHandler
255
256
Bridge between WebSocket connections and ZeroMQ sockets.
257
258
```python
259
from jupyter_server.base.zmqhandlers import ZMQStreamHandler, AuthenticatedZMQStreamHandler
260
```
261
262
```python{ .api }
263
from jupyter_server.base.zmqhandlers import AuthenticatedZMQStreamHandler
264
from jupyter_client.session import Session
265
import zmq
266
267
class MyZMQHandler(AuthenticatedZMQStreamHandler):
268
"""WebSocket handler for ZMQ communication."""
269
270
def initialize(self, zmq_stream, session):
271
"""Initialize with ZMQ stream and session."""
272
self.zmq_stream = zmq_stream
273
self.session = session
274
275
def open(self, kernel_id):
276
"""Handle WebSocket connection."""
277
self.kernel_id = kernel_id
278
self.log.info(f"WebSocket opened for kernel {kernel_id}")
279
280
# Connect to ZMQ socket
281
self.zmq_stream.on_recv(self.on_zmq_reply)
282
283
async def on_message(self, msg):
284
"""Handle WebSocket message."""
285
# Parse WebSocket message
286
try:
287
ws_msg = json.loads(msg)
288
except json.JSONDecodeError:
289
self.log.error("Invalid JSON in WebSocket message")
290
return
291
292
# Forward to ZMQ socket
293
zmq_msg = self.session.serialize(ws_msg)
294
self.zmq_stream.send(zmq_msg)
295
296
def on_zmq_reply(self, msg_list):
297
"""Handle ZMQ reply."""
298
# Deserialize ZMQ message
299
try:
300
msg = self.session.deserialize(msg_list)
301
except Exception as e:
302
self.log.error(f"Failed to deserialize ZMQ message: {e}")
303
return
304
305
# Forward to WebSocket
306
self.write_message(json.dumps(msg))
307
308
def on_close(self):
309
"""Handle WebSocket disconnection."""
310
self.log.info(f"WebSocket closed for kernel {self.kernel_id}")
311
312
# Cleanup ZMQ connection
313
if hasattr(self, 'zmq_stream'):
314
self.zmq_stream.close()
315
```
316
317
### WebSocket Mixins
318
319
```python{ .api }
320
from jupyter_server.base.zmqhandlers import WebSocketMixin
321
from tornado import websocket
322
import json
323
324
class MyWebSocketHandler(WebSocketMixin, websocket.WebSocketHandler):
325
"""Custom WebSocket handler with utilities."""
326
327
def open(self):
328
"""Handle connection."""
329
self.log.info("WebSocket connection opened")
330
self.ping_interval = self.settings.get("websocket_ping_interval", 30)
331
self.start_ping()
332
333
def on_message(self, message):
334
"""Handle incoming message."""
335
try:
336
data = json.loads(message)
337
msg_type = data.get("type")
338
339
if msg_type == "ping":
340
self.send_message({"type": "pong"})
341
elif msg_type == "echo":
342
self.send_message({"type": "echo", "data": data.get("data")})
343
else:
344
self.log.warning(f"Unknown message type: {msg_type}")
345
346
except json.JSONDecodeError:
347
self.send_error("Invalid JSON message")
348
349
def on_close(self):
350
"""Handle disconnection."""
351
self.log.info("WebSocket connection closed")
352
self.stop_ping()
353
354
def send_message(self, message):
355
"""Send JSON message to client."""
356
self.write_message(json.dumps(message))
357
358
def send_error(self, error_message):
359
"""Send error message to client."""
360
self.send_message({
361
"type": "error",
362
"message": error_message,
363
})
364
365
def start_ping(self):
366
"""Start periodic ping to keep connection alive."""
367
import tornado.ioloop
368
369
def send_ping():
370
if not self.ws_connection or self.ws_connection.is_closing():
371
return
372
373
self.ping(b"keepalive")
374
tornado.ioloop.IOLoop.current().call_later(
375
self.ping_interval, send_ping
376
)
377
378
tornado.ioloop.IOLoop.current().call_later(
379
self.ping_interval, send_ping
380
)
381
382
def stop_ping(self):
383
"""Stop ping timer."""
384
# Cleanup would happen automatically when connection closes
385
pass
386
```
387
388
## Error Handlers
389
390
### Template404
391
392
Custom 404 error pages with templating.
393
394
```python{ .api }
395
from jupyter_server.base.handlers import Template404
396
397
# Custom 404 handler
398
class My404Handler(Template404):
399
"""Custom 404 error page."""
400
401
def get_template_path(self):
402
"""Return path to error templates."""
403
return "/path/to/error/templates"
404
405
def get_template_name(self):
406
"""Return 404 template name."""
407
return "my_404.html"
408
409
def get_template_vars(self):
410
"""Return variables for template rendering."""
411
return {
412
"path": self.request.path,
413
"method": self.request.method,
414
"user": self.current_user,
415
"app_name": "My Jupyter App",
416
}
417
418
# Register as default handler
419
handlers = [
420
# ... other handlers
421
(r".*", My404Handler), # Catch-all for 404s
422
]
423
```
424
425
## Utility Functions
426
427
### URL Utilities
428
429
```python
430
from jupyter_server.utils import (
431
url_path_join,
432
url_is_absolute,
433
path2url,
434
url2path,
435
url_escape,
436
url_unescape,
437
)
438
```
439
440
```python{ .api }
441
from jupyter_server.utils import (
442
url_path_join,
443
url_is_absolute,
444
path2url,
445
url2path,
446
url_escape,
447
url_unescape,
448
to_os_path,
449
to_api_path,
450
)
451
452
# URL path joining
453
base_url = "/api/v1"
454
endpoint = "contents/file.txt"
455
full_url = url_path_join(base_url, endpoint)
456
print(full_url) # "/api/v1/contents/file.txt"
457
458
# URL validation
459
is_abs = url_is_absolute("https://example.com/path")
460
print(is_abs) # True
461
462
is_rel = url_is_absolute("/relative/path")
463
print(is_rel) # False
464
465
# Path conversions
466
file_path = "/home/user/notebook.ipynb"
467
api_path = path2url(file_path)
468
print(api_path) # "home/user/notebook.ipynb"
469
470
# URL encoding/decoding
471
encoded = url_escape("file with spaces.txt")
472
print(encoded) # "file%20with%20spaces.txt"
473
474
decoded = url_unescape("file%20with%20spaces.txt")
475
print(decoded) # "file with spaces.txt"
476
477
# OS path conversions
478
api_path = "notebooks/example.ipynb"
479
os_path = to_os_path(api_path, root="/home/user")
480
print(os_path) # "/home/user/notebooks/example.ipynb"
481
482
os_path = "/home/user/notebooks/example.ipynb"
483
api_path = to_api_path(os_path, root="/home/user")
484
print(api_path) # "notebooks/example.ipynb"
485
```
486
487
### System Utilities
488
489
```python{ .api }
490
from jupyter_server.utils import (
491
samefile_simple,
492
check_version,
493
run_sync,
494
fetch,
495
import_item,
496
expand_path,
497
filefind,
498
)
499
500
# File comparison
501
are_same = samefile_simple("/path/to/file1", "/path/to/file2")
502
print(f"Files are same: {are_same}")
503
504
# Version checking
505
is_compatible = check_version("1.0.0", ">=0.9.0")
506
print(f"Version compatible: {is_compatible}")
507
508
# Run async function synchronously
509
async def async_task():
510
return "Result from async function"
511
512
result = run_sync(async_task())
513
print(result) # "Result from async function"
514
515
# HTTP client
516
response = await fetch("https://api.example.com/data")
517
data = response.json()
518
519
# Dynamic import
520
MyClass = import_item("my_package.module.MyClass")
521
instance = MyClass()
522
523
# Path expansion
524
expanded = expand_path("~/documents/notebook.ipynb")
525
print(expanded) # "/home/user/documents/notebook.ipynb"
526
527
# File finding
528
found_file = filefind("config.json", ["/etc", "/usr/local/etc", "~/.config"])
529
print(f"Config found at: {found_file}")
530
```
531
532
### Async Utilities
533
534
```python{ .api }
535
from jupyter_server.utils import ensure_async, run_sync_in_loop
536
import asyncio
537
538
# Ensure function is async
539
def sync_function():
540
return "sync result"
541
542
async_func = ensure_async(sync_function)
543
result = await async_func() # "sync result"
544
545
# Run async in existing event loop
546
async def run_in_background():
547
loop = asyncio.get_event_loop()
548
549
def background_task():
550
# Some sync work
551
return "background result"
552
553
result = await run_sync_in_loop(background_task)
554
print(result)
555
```
556
557
### Unix Socket Support
558
559
```python{ .api }
560
from jupyter_server.utils import (
561
urlencode_unix_socket_path,
562
urldecode_unix_socket_path,
563
unix_socket_in_use,
564
)
565
566
# Unix socket path encoding for URLs
567
socket_path = "/tmp/jupyter.sock"
568
encoded = urlencode_unix_socket_path(socket_path)
569
print(encoded) # Encoded socket path for URL use
570
571
# Decode socket path from URL
572
decoded = urldecode_unix_socket_path(encoded)
573
print(decoded) # "/tmp/jupyter.sock"
574
575
# Check if socket is in use
576
in_use = unix_socket_in_use(socket_path)
577
print(f"Socket in use: {in_use}")
578
```
579
580
## Handler Testing Utilities
581
582
### Test Client
583
584
```python{ .api }
585
from jupyter_server.serverapp import ServerApp
586
from tornado.httpclient import AsyncHTTPClient
587
from tornado.testing import AsyncHTTPTestCase
588
import json
589
590
class MyHandlerTest(AsyncHTTPTestCase):
591
"""Test case for custom handlers."""
592
593
def get_app(self):
594
"""Create test application."""
595
self.server_app = ServerApp()
596
self.server_app.initialize()
597
return self.server_app.web_app
598
599
async def test_api_endpoint(self):
600
"""Test API endpoint."""
601
# Make GET request
602
response = await self.fetch(
603
"/api/my_endpoint",
604
method="GET",
605
headers={"Authorization": "Bearer test-token"},
606
)
607
608
self.assertEqual(response.code, 200)
609
610
# Parse JSON response
611
data = json.loads(response.body)
612
self.assertIn("result", data)
613
614
async def test_post_endpoint(self):
615
"""Test POST endpoint."""
616
payload = {"name": "test", "value": 123}
617
618
response = await self.fetch(
619
"/api/my_endpoint",
620
method="POST",
621
headers={
622
"Content-Type": "application/json",
623
"Authorization": "Bearer test-token",
624
},
625
body=json.dumps(payload),
626
)
627
628
self.assertEqual(response.code, 201)
629
data = json.loads(response.body)
630
self.assertEqual(data["name"], "test")
631
632
async def test_websocket_connection(self):
633
"""Test WebSocket connection."""
634
from tornado.websocket import websocket_connect
635
636
ws_url = f"ws://localhost:{self.get_http_port()}/api/websocket"
637
ws = await websocket_connect(ws_url)
638
639
# Send message
640
test_message = {"type": "test", "data": "hello"}
641
ws.write_message(json.dumps(test_message))
642
643
# Receive response
644
response = await ws.read_message()
645
data = json.loads(response)
646
647
self.assertEqual(data["type"], "response")
648
ws.close()
649
```
650
651
## Performance and Monitoring
652
653
### Metrics Handler
654
655
```python{ .api }
656
from jupyter_server.base.handlers import PrometheusMetricsHandler
657
658
# Built-in metrics endpoint
659
# GET /api/metrics returns Prometheus-format metrics
660
661
# Custom metrics in handlers
662
class MetricsHandler(APIHandler):
663
"""Handler with custom metrics."""
664
665
def initialize(self):
666
self.request_count = 0
667
self.error_count = 0
668
669
async def get(self):
670
"""GET with metrics tracking."""
671
start_time = time.time()
672
self.request_count += 1
673
674
try:
675
# Handle request
676
result = await self.process_request()
677
self.finish(result)
678
679
except Exception as e:
680
self.error_count += 1
681
raise
682
683
finally:
684
# Log performance
685
duration = time.time() - start_time
686
self.log.info(
687
f"Request completed in {duration:.3f}s "
688
f"(total: {self.request_count}, errors: {self.error_count})"
689
)
690
```
691
692
### Request Logging
693
694
```python{ .api }
695
from jupyter_server.base.handlers import JupyterHandler
696
import time
697
698
class LoggedHandler(JupyterHandler):
699
"""Handler with detailed request logging."""
700
701
def prepare(self):
702
"""Log request start."""
703
self.start_time = time.time()
704
self.log.info(
705
f"Request started: {self.request.method} {self.request.path} "
706
f"from {self.request.remote_ip}"
707
)
708
709
def on_finish(self):
710
"""Log request completion."""
711
duration = time.time() - self.start_time
712
status = self.get_status()
713
714
self.log.info(
715
f"Request finished: {self.request.method} {self.request.path} "
716
f"-> {status} in {duration:.3f}s "
717
f"({self.request.body_size} bytes in, {self._write_buffer_size} bytes out)"
718
)
719
720
def write_error(self, status_code, **kwargs):
721
"""Log errors."""
722
self.log.error(
723
f"Request error: {self.request.method} {self.request.path} "
724
f"-> {status_code} {self._reason}"
725
)
726
super().write_error(status_code, **kwargs)
727
```