0
# Exception Handling
1
2
Comprehensive exception hierarchy for robust error handling across all parsing operations. Python-multipart provides specific exception types for different error conditions, enabling precise error handling and debugging.
3
4
## Exception Hierarchy
5
6
```
7
ValueError
8
└── FormParserError (base exception)
9
├── ParseError
10
│ ├── MultipartParseError
11
│ ├── QuerystringParseError
12
│ └── DecodeError
13
└── FileError (also inherits from OSError)
14
```
15
16
## Capabilities
17
18
### Base Exceptions
19
20
#### FormParserError
21
22
Base error class for all form parser exceptions.
23
24
```python { .api }
25
class FormParserError(ValueError):
26
"""
27
Base error class for form parser.
28
Inherits from ValueError for compatibility.
29
"""
30
```
31
32
#### ParseError
33
34
Base class for parsing-related errors with offset tracking.
35
36
```python { .api }
37
class ParseError(FormParserError):
38
"""
39
Base exception for parsing errors.
40
41
Attributes:
42
- offset: Position in input data where error occurred (-1 if not specified)
43
"""
44
45
offset = -1 # Offset in input data chunk where parse error occurred
46
```
47
48
### Specific Parse Errors
49
50
#### MultipartParseError
51
52
Specific error raised when MultipartParser detects parsing errors in multipart/form-data content.
53
54
```python { .api }
55
class MultipartParseError(ParseError):
56
"""
57
Error raised by MultipartParser when detecting parse errors.
58
Used for malformed multipart boundaries, headers, or structure.
59
"""
60
```
61
62
#### QuerystringParseError
63
64
Specific error raised when QuerystringParser detects parsing errors in URL-encoded content.
65
66
```python { .api }
67
class QuerystringParseError(ParseError):
68
"""
69
Error raised by QuerystringParser when detecting parse errors.
70
Used for malformed URL-encoded data or invalid characters.
71
"""
72
```
73
74
#### DecodeError
75
76
Error raised when content decoders encounter invalid encoded data.
77
78
```python { .api }
79
class DecodeError(ParseError):
80
"""
81
Error raised by Base64Decoder or QuotedPrintableDecoder.
82
Used for invalid Base64 data or decoding failures.
83
"""
84
```
85
86
#### FileError
87
88
Exception for problems with File class operations, inheriting from both FormParserError and OSError.
89
90
```python { .api }
91
class FileError(FormParserError, OSError):
92
"""
93
Exception for File class problems.
94
Combines form parsing context with OS-level error information.
95
"""
96
```
97
98
## Usage Examples
99
100
### Basic Exception Handling
101
102
```python
103
from python_multipart import parse_form
104
from python_multipart.exceptions import (
105
FormParserError,
106
MultipartParseError,
107
DecodeError,
108
FileError
109
)
110
111
def safe_form_parsing(headers, input_stream):
112
"""Demonstrate comprehensive exception handling."""
113
114
fields = []
115
files = []
116
errors = []
117
118
def on_field(field):
119
try:
120
fields.append({
121
'name': field.field_name.decode('utf-8'),
122
'value': field.value.decode('utf-8') if field.value else None
123
})
124
except UnicodeDecodeError as e:
125
errors.append(f"Field encoding error: {e}")
126
finally:
127
field.close()
128
129
def on_file(file):
130
try:
131
files.append({
132
'field_name': file.field_name.decode('utf-8'),
133
'filename': file.file_name.decode('utf-8') if file.file_name else None,
134
'size': file.size
135
})
136
except UnicodeDecodeError as e:
137
errors.append(f"File name encoding error: {e}")
138
except Exception as e:
139
errors.append(f"File processing error: {e}")
140
finally:
141
file.close()
142
143
try:
144
parse_form(headers, input_stream, on_field, on_file)
145
146
return {
147
'success': True,
148
'fields': fields,
149
'files': files,
150
'errors': errors
151
}
152
153
except MultipartParseError as e:
154
return {
155
'success': False,
156
'error': 'Malformed multipart data',
157
'details': str(e),
158
'offset': getattr(e, 'offset', -1)
159
}
160
161
except DecodeError as e:
162
return {
163
'success': False,
164
'error': 'Content decoding failed',
165
'details': str(e),
166
'offset': getattr(e, 'offset', -1)
167
}
168
169
except FileError as e:
170
return {
171
'success': False,
172
'error': 'File handling error',
173
'details': str(e),
174
'errno': getattr(e, 'errno', None)
175
}
176
177
except FormParserError as e:
178
return {
179
'success': False,
180
'error': 'Form parsing error',
181
'details': str(e)
182
}
183
184
except Exception as e:
185
return {
186
'success': False,
187
'error': 'Unexpected error',
188
'details': str(e)
189
}
190
```
191
192
### Parser-Specific Error Handling
193
194
```python
195
from python_multipart import MultipartParser, QuerystringParser
196
from python_multipart.exceptions import MultipartParseError, QuerystringParseError
197
198
def handle_multipart_errors(boundary, data_stream):
199
"""Handle MultipartParser specific errors."""
200
201
def on_part_data(data, start, end):
202
# Process part data
203
chunk = data[start:end]
204
print(f"Processing {len(chunk)} bytes")
205
206
callbacks = {'on_part_data': on_part_data}
207
208
try:
209
parser = MultipartParser(boundary, callbacks)
210
211
while True:
212
chunk = data_stream.read(1024)
213
if not chunk:
214
break
215
parser.write(chunk)
216
217
parser.finalize()
218
return {'success': True}
219
220
except MultipartParseError as e:
221
error_info = {
222
'success': False,
223
'error_type': 'multipart_parse_error',
224
'message': str(e),
225
'offset': e.offset
226
}
227
228
# Provide context based on offset
229
if e.offset >= 0:
230
error_info['context'] = f"Error at byte position {e.offset}"
231
232
return error_info
233
234
def handle_querystring_errors(data_stream):
235
"""Handle QuerystringParser specific errors."""
236
237
fields = {}
238
current_field = None
239
current_value = b''
240
241
def on_field_name(data, start, end):
242
nonlocal current_field
243
current_field = data[start:end].decode('utf-8')
244
245
def on_field_data(data, start, end):
246
nonlocal current_value
247
current_value += data[start:end]
248
249
def on_field_end():
250
nonlocal current_field, current_value
251
if current_field:
252
fields[current_field] = current_value.decode('utf-8')
253
current_field = None
254
current_value = b''
255
256
callbacks = {
257
'on_field_name': on_field_name,
258
'on_field_data': on_field_data,
259
'on_field_end': on_field_end
260
}
261
262
try:
263
parser = QuerystringParser(callbacks, strict_parsing=True)
264
265
while True:
266
chunk = data_stream.read(1024)
267
if not chunk:
268
break
269
parser.write(chunk)
270
271
parser.finalize()
272
return {'success': True, 'fields': fields}
273
274
except QuerystringParseError as e:
275
return {
276
'success': False,
277
'error_type': 'querystring_parse_error',
278
'message': str(e),
279
'offset': e.offset,
280
'partial_fields': fields
281
}
282
```
283
284
### Decoder Error Handling
285
286
```python
287
from python_multipart.decoders import Base64Decoder, QuotedPrintableDecoder
288
from python_multipart.exceptions import DecodeError
289
import io
290
291
def safe_base64_decode(encoded_data):
292
"""Safely decode Base64 data with error handling."""
293
294
output = io.BytesIO()
295
decoder = Base64Decoder(output)
296
297
try:
298
decoder.write(encoded_data)
299
decoder.finalize()
300
301
output.seek(0)
302
return {
303
'success': True,
304
'data': output.read()
305
}
306
307
except DecodeError as e:
308
return {
309
'success': False,
310
'error': 'Base64 decode error',
311
'message': str(e),
312
'cache_size': len(decoder.cache) if hasattr(decoder, 'cache') else 0
313
}
314
315
finally:
316
decoder.close()
317
318
def safe_quoted_printable_decode(encoded_data):
319
"""Safely decode quoted-printable data."""
320
321
output = io.BytesIO()
322
decoder = QuotedPrintableDecoder(output)
323
324
try:
325
decoder.write(encoded_data)
326
decoder.finalize()
327
328
output.seek(0)
329
return {
330
'success': True,
331
'data': output.read()
332
}
333
334
except Exception as e: # QuotedPrintableDecoder rarely raises exceptions
335
return {
336
'success': False,
337
'error': 'Quoted-printable decode error',
338
'message': str(e)
339
}
340
341
finally:
342
decoder.close()
343
344
# Test error handling
345
malformed_base64 = b"SGVsbG8gV29ybGQ!" # Invalid padding
346
result = safe_base64_decode(malformed_base64)
347
print(f"Base64 decode result: {result}")
348
```
349
350
### File Error Handling
351
352
```python
353
from python_multipart import File
354
from python_multipart.exceptions import FileError
355
import os
356
import tempfile
357
358
def safe_file_handling(file_name, field_name, content):
359
"""Demonstrate file error handling."""
360
361
# Configure file to use a restricted directory
362
config = {
363
'UPLOAD_DIR': '/restricted/path', # This might not exist
364
'MAX_MEMORY_FILE_SIZE': 1024
365
}
366
367
try:
368
file_obj = File(file_name.encode(), field_name.encode(), config)
369
370
# Write content
371
file_obj.write(content)
372
file_obj.finalize()
373
374
result = {
375
'success': True,
376
'file_name': file_name,
377
'size': file_obj.size,
378
'in_memory': file_obj.in_memory
379
}
380
381
if not file_obj.in_memory:
382
result['temp_path'] = file_obj.actual_file_name.decode()
383
384
file_obj.close()
385
return result
386
387
except FileError as e:
388
return {
389
'success': False,
390
'error_type': 'file_error',
391
'message': str(e),
392
'errno': getattr(e, 'errno', None),
393
'filename': getattr(e, 'filename', None)
394
}
395
396
except OSError as e:
397
return {
398
'success': False,
399
'error_type': 'os_error',
400
'message': str(e),
401
'errno': e.errno
402
}
403
404
except Exception as e:
405
return {
406
'success': False,
407
'error_type': 'unexpected_error',
408
'message': str(e)
409
}
410
411
# Test file error handling
412
result = safe_file_handling('test.txt', 'upload', b'File content here')
413
print(f"File handling result: {result}")
414
```
415
416
### Error Recovery Strategies
417
418
```python
419
from python_multipart import FormParser
420
from python_multipart.exceptions import FormParserError, ParseError
421
import logging
422
423
class RobustFormParser:
424
"""Form parser with error recovery and logging."""
425
426
def __init__(self):
427
self.logger = logging.getLogger(__name__)
428
self.error_count = 0
429
self.max_errors = 10
430
431
def parse_with_recovery(self, headers, input_stream):
432
"""Parse form data with error recovery."""
433
434
fields = []
435
files = []
436
errors = []
437
438
def on_field(field):
439
try:
440
fields.append({
441
'name': field.field_name.decode('utf-8', errors='replace'),
442
'value': field.value.decode('utf-8', errors='replace') if field.value else None
443
})
444
except Exception as e:
445
self.logger.warning(f"Field processing error: {e}")
446
errors.append(f"Field error: {e}")
447
finally:
448
field.close()
449
450
def on_file(file):
451
try:
452
files.append({
453
'field_name': file.field_name.decode('utf-8', errors='replace'),
454
'filename': file.file_name.decode('utf-8', errors='replace') if file.file_name else None,
455
'size': file.size
456
})
457
except Exception as e:
458
self.logger.warning(f"File processing error: {e}")
459
errors.append(f"File error: {e}")
460
finally:
461
file.close()
462
463
try:
464
# Try parsing with error recovery
465
parser = FormParser(
466
headers.get('Content-Type', 'application/octet-stream'),
467
on_field,
468
on_file
469
)
470
471
chunk_count = 0
472
while True:
473
try:
474
chunk = input_stream.read(8192)
475
if not chunk:
476
break
477
478
parser.write(chunk)
479
chunk_count += 1
480
481
except ParseError as e:
482
self.error_count += 1
483
self.logger.error(f"Parse error in chunk {chunk_count}: {e}")
484
errors.append(f"Parse error at chunk {chunk_count}: {e}")
485
486
if self.error_count >= self.max_errors:
487
raise Exception("Too many parse errors, aborting")
488
489
# Continue with next chunk
490
continue
491
492
parser.finalize()
493
parser.close()
494
495
return {
496
'success': True,
497
'fields': fields,
498
'files': files,
499
'errors': errors,
500
'error_count': self.error_count
501
}
502
503
except Exception as e:
504
self.logger.error(f"Fatal parsing error: {e}")
505
return {
506
'success': False,
507
'error': str(e),
508
'partial_fields': fields,
509
'partial_files': files,
510
'errors': errors
511
}
512
513
# Usage
514
parser = RobustFormParser()
515
# result = parser.parse_with_recovery(headers, stream)
516
```