Skip to content

Commit c7323eb

Browse files
feat: Add SDKError wrapping to surface actual request URL in API errors
This change catches SDKError exceptions from the Speakeasy SDK and wraps them with additional context including the actual request URL that was attempted. This makes debugging API issues (like 404 errors) much easier by showing exactly which URL was requested. Functions updated with SDKError wrapping: - get_workspace - list_connections - list_workspaces - list_sources - list_destinations - get_connection The error context now includes: - request_url: The actual URL that was requested - request_method: The HTTP method used - status_code: The HTTP status code - response_content_type: The content type of the response - api_root: The configured API root - workspace_id/connection_id: Resource identifiers Co-Authored-By: AJ Steers <aj@airbyte.io>
1 parent bd07969 commit c7323eb

File tree

1 file changed

+107
-34
lines changed

1 file changed

+107
-34
lines changed

airbyte/_util/api_util.py

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import airbyte_api
2121
import requests
2222
from airbyte_api import api, models
23+
from airbyte_api.errors import SDKError
2324

2425
from airbyte.constants import CLOUD_API_ROOT, CLOUD_CONFIG_API_ROOT, CLOUD_CONFIG_API_ROOT_ENV_VAR
2526
from airbyte.exceptions import (
@@ -58,6 +59,42 @@ def status_ok(status_code: int) -> bool:
5859
return status_code >= 200 and status_code < 300 # noqa: PLR2004 # allow inline magic numbers
5960

6061

62+
def _get_sdk_error_context(error: SDKError) -> dict[str, Any]:
63+
"""Extract context information from an SDKError for debugging.
64+
65+
This helper extracts the actual request URL and other useful debugging
66+
information from the Speakeasy SDK's SDKError exception. The SDK stores
67+
the raw response object which contains the request details.
68+
"""
69+
context: dict[str, Any] = {
70+
"status_code": error.status_code,
71+
"error_message": error.message,
72+
}
73+
74+
if error.raw_response is not None:
75+
request = error.raw_response.request
76+
if request is not None:
77+
context["request_url"] = str(request.url)
78+
context["request_method"] = request.method
79+
context["response_content_type"] = error.raw_response.headers.get("content-type")
80+
81+
return context
82+
83+
84+
def _wrap_sdk_error(error: SDKError, base_context: dict[str, Any] | None = None) -> AirbyteError:
85+
"""Wrap an SDKError with additional context for debugging.
86+
87+
This function converts a Speakeasy SDK error into an AirbyteError with
88+
full URL context, making it easier to debug API issues like 404 errors.
89+
"""
90+
sdk_context = _get_sdk_error_context(error)
91+
merged_context = {**(base_context or {}), **sdk_context}
92+
return AirbyteError(
93+
message=f"API error occurred: {error.message}",
94+
context=merged_context,
95+
)
96+
97+
6198
def get_config_api_root(api_root: str) -> str:
6299
"""Get the configuration API root from the main API root.
63100
@@ -185,11 +222,16 @@ def get_workspace(
185222
client_secret=client_secret,
186223
bearer_token=bearer_token,
187224
)
188-
response = airbyte_instance.workspaces.get_workspace(
189-
api.GetWorkspaceRequest(
190-
workspace_id=workspace_id,
191-
),
192-
)
225+
base_context = {"workspace_id": workspace_id, "api_root": api_root}
226+
try:
227+
response = airbyte_instance.workspaces.get_workspace(
228+
api.GetWorkspaceRequest(
229+
workspace_id=workspace_id,
230+
),
231+
)
232+
except SDKError as e:
233+
raise _wrap_sdk_error(e, base_context) from e
234+
193235
if status_ok(response.status_code) and response.workspace_response:
194236
return response.workspace_response
195237

@@ -232,14 +274,19 @@ def list_connections(
232274
result: list[models.ConnectionResponse] = []
233275
has_more = True
234276
offset, page_size = 0, 100
277+
base_context = {"workspace_id": workspace_id, "api_root": api_root}
235278
while has_more:
236-
response = airbyte_instance.connections.list_connections(
237-
api.ListConnectionsRequest(
238-
workspace_ids=[workspace_id],
239-
offset=offset,
240-
limit=page_size,
241-
),
242-
)
279+
try:
280+
response = airbyte_instance.connections.list_connections(
281+
api.ListConnectionsRequest(
282+
workspace_ids=[workspace_id],
283+
offset=offset,
284+
limit=page_size,
285+
),
286+
)
287+
except SDKError as e:
288+
raise _wrap_sdk_error(e, base_context) from e
289+
243290
has_more = bool(response.connections_response and response.connections_response.next)
244291
offset += page_size
245292

@@ -286,10 +333,17 @@ def list_workspaces(
286333
result: list[models.WorkspaceResponse] = []
287334
has_more = True
288335
offset, page_size = 0, 100
336+
base_context = {"workspace_id": workspace_id, "api_root": api_root}
289337
while has_more:
290-
response: api.ListWorkspacesResponse = airbyte_instance.workspaces.list_workspaces(
291-
api.ListWorkspacesRequest(workspace_ids=[workspace_id], offset=offset, limit=page_size),
292-
)
338+
try:
339+
response: api.ListWorkspacesResponse = airbyte_instance.workspaces.list_workspaces(
340+
api.ListWorkspacesRequest(
341+
workspace_ids=[workspace_id], offset=offset, limit=page_size
342+
),
343+
)
344+
except SDKError as e:
345+
raise _wrap_sdk_error(e, base_context) from e
346+
293347
has_more = bool(response.workspaces_response and response.workspaces_response.next)
294348
offset += page_size
295349

@@ -338,14 +392,19 @@ def list_sources(
338392
result: list[models.SourceResponse] = []
339393
has_more = True
340394
offset, page_size = 0, 100
395+
base_context = {"workspace_id": workspace_id, "api_root": api_root}
341396
while has_more:
342-
response: api.ListSourcesResponse = airbyte_instance.sources.list_sources(
343-
api.ListSourcesRequest(
344-
workspace_ids=[workspace_id],
345-
offset=offset,
346-
limit=page_size,
347-
),
348-
)
397+
try:
398+
response: api.ListSourcesResponse = airbyte_instance.sources.list_sources(
399+
api.ListSourcesRequest(
400+
workspace_ids=[workspace_id],
401+
offset=offset,
402+
limit=page_size,
403+
),
404+
)
405+
except SDKError as e:
406+
raise _wrap_sdk_error(e, base_context) from e
407+
349408
has_more = bool(response.sources_response and response.sources_response.next)
350409
offset += page_size
351410

@@ -389,14 +448,19 @@ def list_destinations(
389448
result: list[models.DestinationResponse] = []
390449
has_more = True
391450
offset, page_size = 0, 100
451+
base_context = {"workspace_id": workspace_id, "api_root": api_root}
392452
while has_more:
393-
response = airbyte_instance.destinations.list_destinations(
394-
api.ListDestinationsRequest(
395-
workspace_ids=[workspace_id],
396-
offset=offset,
397-
limit=page_size,
398-
),
399-
)
453+
try:
454+
response = airbyte_instance.destinations.list_destinations(
455+
api.ListDestinationsRequest(
456+
workspace_ids=[workspace_id],
457+
offset=offset,
458+
limit=page_size,
459+
),
460+
)
461+
except SDKError as e:
462+
raise _wrap_sdk_error(e, base_context) from e
463+
400464
has_more = bool(response.destinations_response and response.destinations_response.next)
401465
offset += page_size
402466

@@ -438,11 +502,20 @@ def get_connection(
438502
bearer_token=bearer_token,
439503
api_root=api_root,
440504
)
441-
response = airbyte_instance.connections.get_connection(
442-
api.GetConnectionRequest(
443-
connection_id=connection_id,
444-
),
445-
)
505+
base_context = {
506+
"workspace_id": workspace_id,
507+
"connection_id": connection_id,
508+
"api_root": api_root,
509+
}
510+
try:
511+
response = airbyte_instance.connections.get_connection(
512+
api.GetConnectionRequest(
513+
connection_id=connection_id,
514+
),
515+
)
516+
except SDKError as e:
517+
raise _wrap_sdk_error(e, base_context) from e
518+
446519
if status_ok(response.status_code) and response.connection_response:
447520
return response.connection_response
448521

0 commit comments

Comments
 (0)