or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

auth.mdconfiguration.mdcore-application.mdextensions.mdhandlers.mdindex.mdservices.md

handlers.mddocs/

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

```