or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

context-management.mderror-handling.mdindex.mdmiddleware.mdplugins.md

error-handling.mddocs/

0

# Error Handling

1

2

Comprehensive error handling for context lifecycle, configuration, and plugin validation. The starlette-context library provides custom exceptions with support for custom error responses and detailed error messages.

3

4

## Capabilities

5

6

### Base Exception Classes

7

8

Foundation exception classes that establish the error hierarchy for the library.

9

10

```python { .api }

11

class StarletteContextError(Exception):

12

"""Base exception class for all starlette-context errors."""

13

pass

14

15

class ContextDoesNotExistError(RuntimeError, StarletteContextError):

16

"""

17

Raised when context is accessed outside of a request-response cycle.

18

19

This occurs when:

20

- Context is accessed before middleware has created it

21

- Context is accessed after request processing is complete

22

- Context is accessed in code not running within a request context

23

"""

24

25

def __init__(self) -> None:

26

self.message = (

27

"You didn't use the required middleware or "

28

"you're trying to access `context` object "

29

"outside of the request-response cycle."

30

)

31

super().__init__(self.message)

32

33

class ConfigurationError(StarletteContextError):

34

"""

35

Raised for configuration errors in middleware or plugins.

36

37

Common causes:

38

- Invalid plugin instances passed to middleware

39

- Unsupported UUID versions in UUID plugins

40

- Invalid middleware configuration

41

"""

42

pass

43

```

44

45

### Middleware Validation Errors

46

47

Errors that occur during request processing and can return custom HTTP responses.

48

49

```python { .api }

50

class MiddleWareValidationError(StarletteContextError):

51

"""

52

Base class for middleware validation errors.

53

54

These errors can include custom HTTP responses that will be returned

55

to the client instead of the default error response.

56

"""

57

58

def __init__(

59

self, *args: Any, error_response: Optional[Response] = None

60

) -> None:

61

"""

62

Initialize validation error.

63

64

Parameters:

65

- *args: Exception arguments

66

- error_response: Optional custom HTTP response for this error

67

"""

68

super().__init__(*args)

69

self.error_response = error_response

70

71

class WrongUUIDError(MiddleWareValidationError):

72

"""

73

Raised when UUID validation fails in UUID-based plugins.

74

75

Occurs when:

76

- Invalid UUID format in request headers

77

- UUID version mismatch

78

- Malformed UUID strings

79

"""

80

pass

81

82

class DateFormatError(MiddleWareValidationError):

83

"""

84

Raised when date header parsing fails in DateHeaderPlugin.

85

86

Occurs when:

87

- Invalid RFC1123 date format

88

- Non-GMT timezone specified

89

- Malformed date strings

90

"""

91

pass

92

```

93

94

## Usage Examples

95

96

### Context Existence Checking

97

98

```python

99

from starlette_context import context

100

from starlette_context.errors import ContextDoesNotExistError

101

102

def safe_context_access():

103

"""Safely access context with error handling."""

104

try:

105

user_id = context["user_id"]

106

return f"User: {user_id}"

107

except ContextDoesNotExistError:

108

return "Context not available"

109

110

# Better approach: check existence first

111

def preferred_safe_access():

112

"""Preferred way to safely access context."""

113

if context.exists():

114

return context.get("user_id", "No user ID")

115

return "Context not available"

116

```

117

118

### Plugin Configuration Errors

119

120

```python

121

from starlette_context.middleware import ContextMiddleware

122

from starlette_context.errors import ConfigurationError

123

from starlette_context.plugins import RequestIdPlugin

124

125

try:

126

# This will raise ConfigurationError - invalid plugin

127

app.add_middleware(

128

ContextMiddleware,

129

plugins=["not_a_plugin", RequestIdPlugin()] # String is not a Plugin

130

)

131

except ConfigurationError as e:

132

print(f"Configuration error: {e}")

133

# Fix: Use only valid Plugin instances

134

app.add_middleware(

135

ContextMiddleware,

136

plugins=[RequestIdPlugin()]

137

)

138

```

139

140

### UUID Validation Errors

141

142

```python

143

from starlette_context.plugins import RequestIdPlugin, CorrelationIdPlugin

144

from starlette_context.errors import WrongUUIDError

145

from starlette.responses import JSONResponse

146

147

# Custom error response for UUID validation

148

uuid_error_response = JSONResponse(

149

{

150

"error": "Invalid UUID format",

151

"message": "Request ID must be a valid UUID v4",

152

"example": "550e8400-e29b-41d4-a716-446655440000"

153

},

154

status_code=422

155

)

156

157

# Plugin with custom error response

158

request_id_plugin = RequestIdPlugin(

159

validate=True,

160

error_response=uuid_error_response

161

)

162

163

app.add_middleware(

164

ContextMiddleware,

165

plugins=[request_id_plugin]

166

)

167

168

# Test invalid UUID

169

# Request with header: X-Request-ID: invalid-uuid

170

# Will return the custom JSON error response

171

```

172

173

### Date Format Errors

174

175

```python

176

from starlette_context.plugins import DateHeaderPlugin

177

from starlette_context.errors import DateFormatError

178

from starlette.responses import JSONResponse

179

180

# Custom error response for date parsing

181

date_error_response = JSONResponse(

182

{

183

"error": "Invalid date format",

184

"message": "Date header must be in RFC1123 format",

185

"expected_format": "Wed, 01 Jan 2020 04:27:12 GMT",

186

"received": None # Will be filled by middleware

187

},

188

status_code=422

189

)

190

191

date_plugin = DateHeaderPlugin(error_response=date_error_response)

192

193

app.add_middleware(ContextMiddleware, plugins=[date_plugin])

194

195

# Test invalid date

196

# Request with header: Date: invalid-date-format

197

# Will return the custom JSON error response

198

```

199

200

### Middleware Error Handling

201

202

```python

203

from starlette_context.middleware import ContextMiddleware

204

from starlette_context.errors import MiddleWareValidationError

205

from starlette.responses import JSONResponse

206

207

# Default error response for all validation errors

208

default_error = JSONResponse(

209

{"error": "Request validation failed"},

210

status_code=400

211

)

212

213

app.add_middleware(

214

ContextMiddleware,

215

plugins=[

216

RequestIdPlugin(validate=True), # No custom error response

217

CorrelationIdPlugin(validate=True) # No custom error response

218

],

219

default_error_response=default_error

220

)

221

222

# Validation errors without custom responses will use default_error_response

223

```

224

225

### Custom Plugin Error Handling

226

227

```python

228

from starlette_context.plugins import Plugin

229

from starlette_context.errors import MiddleWareValidationError

230

from starlette.responses import JSONResponse

231

232

class ApiKeyValidationPlugin(Plugin):

233

key = "X-API-Key"

234

235

def __init__(self, valid_keys, error_response=None):

236

self.valid_keys = set(valid_keys)

237

self.error_response = error_response or JSONResponse(

238

{"error": "Invalid API key"},

239

status_code=401

240

)

241

242

async def process_request(self, request):

243

api_key = await self.extract_value_from_header_by_key(request)

244

245

if not api_key:

246

raise MiddleWareValidationError(

247

"Missing API key",

248

error_response=JSONResponse(

249

{"error": "API key required"},

250

status_code=401

251

)

252

)

253

254

if api_key not in self.valid_keys:

255

raise MiddleWareValidationError(

256

"Invalid API key",

257

error_response=self.error_response

258

)

259

260

return api_key

261

262

# Usage

263

api_plugin = ApiKeyValidationPlugin(

264

valid_keys=["key1", "key2", "key3"]

265

)

266

267

app.add_middleware(ContextMiddleware, plugins=[api_plugin])

268

```

269

270

### Error Response Precedence

271

272

The error response precedence order is:

273

274

1. Plugin-specific error response (from exception)

275

2. Plugin's configured error response

276

3. Middleware's default error response

277

278

```python

279

from starlette_context.plugins import PluginUUIDBase

280

from starlette_context.errors import WrongUUIDError

281

from starlette.responses import JSONResponse

282

283

class CustomUUIDPlugin(PluginUUIDBase):

284

key = "X-Custom-ID"

285

286

def __init__(self):

287

# Plugin-level error response

288

super().__init__(

289

validate=True,

290

error_response=JSONResponse(

291

{"error": "Plugin-level error"},

292

status_code=422

293

)

294

)

295

296

async def process_request(self, request):

297

try:

298

return await super().process_request(request)

299

except WrongUUIDError:

300

# Exception-specific error response (highest precedence)

301

raise WrongUUIDError(

302

"Custom UUID validation failed",

303

error_response=JSONResponse(

304

{"error": "Exception-level error"},

305

status_code=400

306

)

307

)

308

309

# Middleware-level default (lowest precedence)

310

app.add_middleware(

311

ContextMiddleware,

312

plugins=[CustomUUIDPlugin()],

313

default_error_response=JSONResponse(

314

{"error": "Middleware-level error"},

315

status_code=500

316

)

317

)

318

```

319

320

### Logging Errors

321

322

```python

323

import logging

324

from starlette_context.middleware import ContextMiddleware

325

from starlette_context.errors import MiddleWareValidationError

326

327

logger = logging.getLogger(__name__)

328

329

class LoggingContextMiddleware(ContextMiddleware):

330

async def dispatch(self, request, call_next):

331

try:

332

return await super().dispatch(request, call_next)

333

except MiddleWareValidationError as e:

334

# Log validation errors

335

logger.warning(

336

f"Context validation error: {e}",

337

extra={

338

"path": request.url.path,

339

"method": request.method,

340

"headers": dict(request.headers)

341

}

342

)

343

# Re-raise to return error response

344

raise

345

346

app.add_middleware(LoggingContextMiddleware, plugins=[...])

347

```

348

349

### Testing Error Scenarios

350

351

```python

352

import pytest

353

from starlette.testclient import TestClient

354

from starlette_context.errors import ContextDoesNotExistError

355

from starlette_context import context

356

357

def test_context_without_middleware():

358

"""Test accessing context without middleware raises error."""

359

with pytest.raises(ContextDoesNotExistError):

360

value = context["key"]

361

362

def test_invalid_uuid_plugin():

363

"""Test UUID plugin with invalid UUID."""

364

client = TestClient(app) # App with RequestIdPlugin(validate=True)

365

366

response = client.get("/", headers={"X-Request-ID": "invalid-uuid"})

367

assert response.status_code == 422

368

assert "Invalid UUID" in response.json()["error"]

369

370

def test_custom_error_response():

371

"""Test plugin with custom error response."""

372

# Setup app with custom error response

373

client = TestClient(app)

374

375

response = client.get("/", headers={"Date": "invalid-date"})

376

assert response.status_code == 422

377

assert response.json()["error"] == "Invalid date format"

378

```

379

380

## Best Practices

381

382

### Error Response Design

383

384

```python

385

# Good: Structured error responses

386

error_response = JSONResponse(

387

{

388

"error": "validation_failed",

389

"message": "Request validation failed",

390

"details": {

391

"field": "X-Request-ID",

392

"expected": "Valid UUID v4",

393

"received": "invalid-format"

394

}

395

},

396

status_code=422

397

)

398

399

# Avoid: Generic or unclear errors

400

error_response = JSONResponse({"error": "Bad request"}, status_code=400)

401

```

402

403

### Error Handling Strategy

404

405

```python

406

# Context access pattern

407

def get_user_context():

408

"""Get user data from context with proper error handling."""

409

if not context.exists():

410

return None

411

412

return {

413

"user_id": context.get("user_id"),

414

"request_id": context.get("X-Request-ID"),

415

"session": context.get("session_id")

416

}

417

418

# Plugin validation pattern

419

class ValidationPlugin(Plugin):

420

def __init__(self, error_response=None):

421

self.error_response = error_response or self.default_error_response

422

423

@property

424

def default_error_response(self):

425

return JSONResponse(

426

{"error": f"Invalid {self.key} header"},

427

status_code=422

428

)

429

430

async def process_request(self, request):

431

value = await self.extract_value_from_header_by_key(request)

432

if not self.validate_value(value):

433

raise MiddleWareValidationError(

434

f"Invalid {self.key}: {value}",

435

error_response=self.error_response

436

)

437

return value

438

```