0
# Error Handling
1
2
Comprehensive error hierarchy covering all API scenarios including network errors, authentication failures, data conflicts, and OSM-specific exceptions. The osmapi library provides detailed error information to help developers handle different failure conditions appropriately.
3
4
## Error Hierarchy
5
6
### Base Error Classes
7
8
```python { .api }
9
class OsmApiError(Exception):
10
"""
11
General OsmApi error class serving as superclass for all other errors.
12
Base exception for all osmapi-specific errors.
13
"""
14
15
class ApiError(OsmApiError):
16
"""
17
Base API request error with detailed response information.
18
19
Attributes:
20
- status (int): HTTP error code
21
- reason (str): Error message
22
- payload (str): Response payload when error occurred
23
"""
24
25
def __init__(self, status, reason, payload):
26
self.status = status
27
self.reason = reason
28
self.payload = payload
29
```
30
31
**Usage Example:**
32
33
```python
34
import osmapi
35
36
api = osmapi.OsmApi()
37
38
try:
39
node = api.NodeGet(999999)
40
except osmapi.ApiError as e:
41
print(f"API Error {e.status}: {e.reason}")
42
if e.payload:
43
print(f"Server response: {e.payload}")
44
except osmapi.OsmApiError as e:
45
print(f"General osmapi error: {e}")
46
```
47
48
## Authentication Errors
49
50
Errors related to authentication and authorization.
51
52
### UsernamePasswordMissingError
53
54
```python { .api }
55
class UsernamePasswordMissingError(OsmApiError):
56
"""
57
Error when username or password is missing for an authenticated request.
58
59
Raised when attempting operations that require authentication
60
without providing credentials.
61
"""
62
```
63
64
**Usage Example:**
65
66
```python
67
import osmapi
68
69
# No authentication provided
70
api = osmapi.OsmApi()
71
72
try:
73
# This requires authentication
74
api.ChangesetCreate({"comment": "Test changeset"})
75
except osmapi.UsernamePasswordMissingError:
76
print("Authentication required for this operation")
77
# Provide credentials
78
api = osmapi.OsmApi(username="user", password="pass")
79
```
80
81
### UnauthorizedApiError
82
83
```python { .api }
84
class UnauthorizedApiError(ApiError):
85
"""
86
Error when the API returned an Unauthorized error.
87
88
Raised when provided OAuth token is expired, invalid credentials
89
are used, or account lacks permissions for the operation.
90
"""
91
```
92
93
**Usage Example:**
94
95
```python
96
import osmapi
97
98
api = osmapi.OsmApi(username="wrong_user", password="wrong_pass")
99
100
try:
101
api.ChangesetCreate({"comment": "Test"})
102
except osmapi.UnauthorizedApiError as e:
103
print(f"Authentication failed: {e.reason}")
104
# Check credentials or OAuth token
105
```
106
107
## Network and Communication Errors
108
109
Errors related to network connectivity and HTTP communication.
110
111
### TimeoutApiError
112
113
```python { .api }
114
class TimeoutApiError(ApiError):
115
"""
116
Error if the HTTP request ran into a timeout.
117
118
Raised when requests exceed the configured timeout period.
119
"""
120
```
121
122
### ConnectionApiError
123
124
```python { .api }
125
class ConnectionApiError(ApiError):
126
"""
127
Error if there was a network error while connecting to the remote server.
128
129
Includes DNS failures, refused connections, and other network issues.
130
"""
131
```
132
133
### MaximumRetryLimitReachedError
134
135
```python { .api }
136
class MaximumRetryLimitReachedError(OsmApiError):
137
"""
138
Error when the maximum amount of retries is reached and we have to give up.
139
140
Raised when all retry attempts are exhausted for server errors.
141
"""
142
```
143
144
**Usage Example:**
145
146
```python
147
import osmapi
148
149
api = osmapi.OsmApi(timeout=5) # Short timeout for example
150
151
try:
152
node = api.NodeGet(123)
153
except osmapi.TimeoutApiError as e:
154
print(f"Request timed out: {e.reason}")
155
# Retry with longer timeout or check network
156
except osmapi.ConnectionApiError as e:
157
print(f"Network error: {e.reason}")
158
# Check internet connection
159
except osmapi.MaximumRetryLimitReachedError as e:
160
print(f"Server unavailable after retries: {e}")
161
```
162
163
## Response and Data Errors
164
165
Errors related to API response processing and data validation.
166
167
### XmlResponseInvalidError
168
169
```python { .api }
170
class XmlResponseInvalidError(OsmApiError):
171
"""
172
Error if the XML response from the OpenStreetMap API is invalid.
173
174
Raised when the server returns malformed XML or unexpected response format.
175
"""
176
```
177
178
### ResponseEmptyApiError
179
180
```python { .api }
181
class ResponseEmptyApiError(ApiError):
182
"""
183
Error when the response to the request is empty.
184
185
Raised when expecting data but receiving empty response from server.
186
"""
187
```
188
189
**Usage Example:**
190
191
```python
192
import osmapi
193
194
api = osmapi.OsmApi()
195
196
try:
197
nodes = api.NodesGet([123, 456])
198
except osmapi.XmlResponseInvalidError as e:
199
print(f"Invalid XML response: {e}")
200
# Server may be experiencing issues
201
except osmapi.ResponseEmptyApiError as e:
202
print(f"Empty response: {e.reason}")
203
# May indicate server problems or invalid request
204
```
205
206
## Changeset Errors
207
208
Errors specific to changeset operations and lifecycle.
209
210
### NoChangesetOpenError
211
212
```python { .api }
213
class NoChangesetOpenError(OsmApiError):
214
"""
215
Error when an operation requires an open changeset, but currently
216
no changeset is open.
217
218
Raised when attempting data modifications without an active changeset.
219
"""
220
```
221
222
### ChangesetAlreadyOpenError
223
224
```python { .api }
225
class ChangesetAlreadyOpenError(OsmApiError):
226
"""
227
Error when a user tries to open a changeset when there is already
228
an open changeset.
229
230
Only one changeset can be open at a time per user session.
231
"""
232
```
233
234
### ChangesetClosedApiError
235
236
```python { .api }
237
class ChangesetClosedApiError(ApiError):
238
"""
239
Error if the changeset in question has already been closed.
240
241
Raised when attempting to modify a changeset that is no longer active.
242
"""
243
```
244
245
**Usage Example:**
246
247
```python
248
import osmapi
249
250
api = osmapi.OsmApi(username="user", password="pass")
251
252
try:
253
# Forgot to open changeset
254
api.NodeCreate({"lat": 0, "lon": 0, "tag": {}})
255
except osmapi.NoChangesetOpenError:
256
print("Need to open a changeset first")
257
changeset_id = api.ChangesetCreate({"comment": "Adding nodes"})
258
259
try:
260
# Try to open second changeset
261
api.ChangesetCreate({"comment": "Another changeset"})
262
except osmapi.ChangesetAlreadyOpenError:
263
print("Close current changeset before opening new one")
264
api.ChangesetClose()
265
```
266
267
## Element Errors
268
269
Errors related to OSM elements (nodes, ways, relations) and their state.
270
271
### ElementNotFoundApiError
272
273
```python { .api }
274
class ElementNotFoundApiError(ApiError):
275
"""
276
Error if the requested element was not found.
277
278
Raised when requesting an element ID that doesn't exist.
279
"""
280
```
281
282
### ElementDeletedApiError
283
284
```python { .api }
285
class ElementDeletedApiError(ApiError):
286
"""
287
Error when the requested element is deleted.
288
289
Raised when requesting an element that has been marked as deleted.
290
"""
291
```
292
293
### OsmTypeAlreadyExistsError
294
295
```python { .api }
296
class OsmTypeAlreadyExistsError(OsmApiError):
297
"""
298
Error when a user tries to create an object that already exists.
299
300
Raised when attempting to create an element with an existing ID.
301
"""
302
```
303
304
**Usage Example:**
305
306
```python
307
import osmapi
308
309
api = osmapi.OsmApi()
310
311
try:
312
node = api.NodeGet(999999999)
313
except osmapi.ElementNotFoundApiError:
314
print("Node doesn't exist")
315
except osmapi.ElementDeletedApiError:
316
print("Node has been deleted")
317
318
# Creating elements
319
api_auth = osmapi.OsmApi(username="user", password="pass")
320
try:
321
with api_auth.Changeset({"comment": "Test"}) as changeset_id:
322
# This would fail if trying to create with existing ID
323
existing_node = {"id": 123, "lat": 0, "lon": 0, "tag": {}}
324
api_auth.NodeCreate(existing_node)
325
except osmapi.OsmTypeAlreadyExistsError:
326
print("Cannot create element with existing ID")
327
```
328
329
## Version and Conflict Errors
330
331
Errors related to version conflicts and data consistency.
332
333
### VersionMismatchApiError
334
335
```python { .api }
336
class VersionMismatchApiError(ApiError):
337
"""
338
Error if the provided version does not match the database version
339
of the element.
340
341
Raised when attempting to modify an element that has been changed
342
by another user since it was last retrieved.
343
"""
344
```
345
346
### PreconditionFailedApiError
347
348
```python { .api }
349
class PreconditionFailedApiError(ApiError):
350
"""
351
Error if the precondition of the operation was not met.
352
353
Raised when:
354
- A way has nodes that do not exist or are not visible
355
- A relation has elements that do not exist or are not visible
356
- A node/way/relation is still used in a way/relation when deleting
357
"""
358
```
359
360
**Usage Example:**
361
362
```python
363
import osmapi
364
365
api = osmapi.OsmApi(username="user", password="pass")
366
367
try:
368
# Get current node
369
node = api.NodeGet(12345)
370
371
with api.Changeset({"comment": "Update node"}) as changeset_id:
372
# Someone else might have modified it meanwhile
373
node["tag"]["updated"] = "yes"
374
api.NodeUpdate(node)
375
376
except osmapi.VersionMismatchApiError as e:
377
print(f"Version conflict: {e.reason}")
378
# Re-fetch current version and merge changes
379
current_node = api.NodeGet(12345)
380
# Apply changes to current version
381
382
try:
383
with api.Changeset({"comment": "Create way"}) as changeset_id:
384
# This will fail if nodes don't exist
385
api.WayCreate({
386
"nd": [999999, 999998], # Non-existent nodes
387
"tag": {"highway": "path"}
388
})
389
except osmapi.PreconditionFailedApiError as e:
390
print(f"Referenced elements don't exist: {e.reason}")
391
```
392
393
## Notes-Specific Errors
394
395
Errors specific to Notes API operations.
396
397
### NoteAlreadyClosedApiError
398
399
```python { .api }
400
class NoteAlreadyClosedApiError(ApiError):
401
"""
402
Error if the note in question has already been closed.
403
404
Raised when attempting to close a note that is already closed.
405
"""
406
```
407
408
**Usage Example:**
409
410
```python
411
import osmapi
412
413
api = osmapi.OsmApi(username="user", password="pass")
414
415
try:
416
api.NoteClose(12345, "Issue resolved")
417
except osmapi.NoteAlreadyClosedApiError:
418
print("Note is already closed")
419
# Maybe reopen it instead
420
api.NoteReopen(12345, "Issue has returned")
421
```
422
423
## Subscription Errors
424
425
Errors related to changeset discussion subscriptions.
426
427
### AlreadySubscribedApiError
428
429
```python { .api }
430
class AlreadySubscribedApiError(ApiError):
431
"""
432
Error when a user tries to subscribe to a changeset
433
that they are already subscribed to.
434
"""
435
```
436
437
### NotSubscribedApiError
438
439
```python { .api }
440
class NotSubscribedApiError(ApiError):
441
"""
442
Error when user tries to unsubscribe from a changeset
443
that they are not subscribed to.
444
"""
445
```
446
447
**Usage Example:**
448
449
```python
450
import osmapi
451
452
api = osmapi.OsmApi(username="user", password="pass")
453
454
try:
455
api.ChangesetSubscribe(12345)
456
except osmapi.AlreadySubscribedApiError:
457
print("Already subscribed to this changeset")
458
459
try:
460
api.ChangesetUnsubscribe(12345)
461
except osmapi.NotSubscribedApiError:
462
print("Not subscribed to this changeset")
463
```
464
465
## Error Handling Strategies
466
467
### Graceful Degradation
468
469
```python
470
import osmapi
471
import time
472
473
def robust_node_get(api, node_id, max_retries=3):
474
"""Get node with retry logic and graceful error handling."""
475
476
for attempt in range(max_retries):
477
try:
478
return api.NodeGet(node_id)
479
480
except osmapi.ElementNotFoundApiError:
481
# Element doesn't exist, no point retrying
482
return None
483
484
except osmapi.ElementDeletedApiError:
485
# Element was deleted, return None or handle specially
486
return {"deleted": True, "id": node_id}
487
488
except (osmapi.TimeoutApiError, osmapi.ConnectionApiError):
489
if attempt < max_retries - 1:
490
# Wait before retry
491
time.sleep(2 ** attempt) # Exponential backoff
492
continue
493
else:
494
# Final attempt failed
495
raise
496
497
except osmapi.MaximumRetryLimitReachedError:
498
# Server is having issues
499
print(f"Server unavailable, giving up on node {node_id}")
500
return None
501
502
return None
503
```
504
505
### Version Conflict Resolution
506
507
```python
508
import osmapi
509
510
def update_with_conflict_resolution(api, element_type, element_data):
511
"""Update element with automatic conflict resolution."""
512
513
max_attempts = 3
514
515
for attempt in range(max_attempts):
516
try:
517
if element_type == "node":
518
return api.NodeUpdate(element_data)
519
elif element_type == "way":
520
return api.WayUpdate(element_data)
521
elif element_type == "relation":
522
return api.RelationUpdate(element_data)
523
524
except osmapi.VersionMismatchApiError:
525
if attempt < max_attempts - 1:
526
# Fetch current version
527
element_id = element_data["id"]
528
529
if element_type == "node":
530
current = api.NodeGet(element_id)
531
elif element_type == "way":
532
current = api.WayGet(element_id)
533
elif element_type == "relation":
534
current = api.RelationGet(element_id)
535
536
# Merge changes (simple example - merge tags)
537
current["tag"].update(element_data["tag"])
538
element_data = current
539
continue
540
else:
541
# Too many conflicts, give up
542
raise
543
544
return None
545
```
546
547
### Comprehensive Error Logging
548
549
```python
550
import osmapi
551
import logging
552
553
# Set up logging
554
logging.basicConfig(level=logging.INFO)
555
logger = logging.getLogger(__name__)
556
557
def safe_osm_operation(operation_func, *args, **kwargs):
558
"""Wrapper for OSM operations with comprehensive error logging."""
559
560
try:
561
result = operation_func(*args, **kwargs)
562
logger.info(f"Operation successful: {operation_func.__name__}")
563
return result
564
565
except osmapi.UsernamePasswordMissingError as e:
566
logger.error(f"Authentication required: {e}")
567
raise
568
569
except osmapi.UnauthorizedApiError as e:
570
logger.error(f"Authentication failed: {e.status} - {e.reason}")
571
raise
572
573
except osmapi.ElementNotFoundApiError as e:
574
logger.warning(f"Element not found: {e.status} - {e.reason}")
575
return None
576
577
except osmapi.VersionMismatchApiError as e:
578
logger.warning(f"Version conflict: {e.status} - {e.reason}")
579
# Could implement retry logic here
580
raise
581
582
except osmapi.ChangesetClosedApiError as e:
583
logger.error(f"Changeset closed: {e.status} - {e.reason}")
584
raise
585
586
except osmapi.TimeoutApiError as e:
587
logger.error(f"Request timeout: {e.reason}")
588
# Could implement retry logic
589
raise
590
591
except osmapi.ApiError as e:
592
logger.error(f"API Error {e.status}: {e.reason}")
593
if e.payload:
594
logger.error(f"Server response: {e.payload}")
595
raise
596
597
except osmapi.OsmApiError as e:
598
logger.error(f"OSM API Error: {e}")
599
raise
600
601
except Exception as e:
602
logger.error(f"Unexpected error: {e}")
603
raise
604
605
# Usage example
606
api = osmapi.OsmApi(username="user", password="pass")
607
608
# Safe operation with logging
609
result = safe_osm_operation(api.NodeGet, 12345)
610
if result:
611
print(f"Got node: {result['id']}")
612
```
613
614
## Error Prevention Best Practices
615
616
### Data Validation
617
618
```python
619
def validate_node_data(node_data):
620
"""Validate node data before API calls."""
621
622
required_fields = ["lat", "lon"]
623
for field in required_fields:
624
if field not in node_data:
625
raise ValueError(f"Missing required field: {field}")
626
627
# Validate coordinate ranges
628
if not -90 <= node_data["lat"] <= 90:
629
raise ValueError(f"Invalid latitude: {node_data['lat']}")
630
631
if not -180 <= node_data["lon"] <= 180:
632
raise ValueError(f"Invalid longitude: {node_data['lon']}")
633
634
# Validate tags
635
if "tag" in node_data:
636
for key, value in node_data["tag"].items():
637
if not isinstance(key, str) or not isinstance(value, str):
638
raise ValueError("Tag keys and values must be strings")
639
```
640
641
### Changeset Management
642
643
```python
644
import osmapi
645
646
class SafeChangesetManager:
647
"""Context manager with enhanced error handling."""
648
649
def __init__(self, api, changeset_tags):
650
self.api = api
651
self.changeset_tags = changeset_tags
652
self.changeset_id = None
653
654
def __enter__(self):
655
try:
656
self.changeset_id = self.api.ChangesetCreate(self.changeset_tags)
657
return self.changeset_id
658
except osmapi.ChangesetAlreadyOpenError:
659
# Close existing changeset and try again
660
self.api.ChangesetClose()
661
self.changeset_id = self.api.ChangesetCreate(self.changeset_tags)
662
return self.changeset_id
663
664
def __exit__(self, exc_type, exc_val, exc_tb):
665
if self.changeset_id:
666
try:
667
self.api.ChangesetClose()
668
except osmapi.ChangesetClosedApiError:
669
# Already closed, that's fine
670
pass
671
except Exception as e:
672
logger.error(f"Error closing changeset: {e}")
673
674
# Usage
675
api = osmapi.OsmApi(username="user", password="pass")
676
with SafeChangesetManager(api, {"comment": "Safe operations"}) as changeset_id:
677
# Perform operations safely
678
pass
679
```