Skip to content
Prev Previous commit
Next Next commit
Updates based on feedback
  • Loading branch information
SamRemis committed Nov 18, 2025
commit 49923880c16839801405c2fab3569a7813654744
Original file line number Diff line number Diff line change
Expand Up @@ -619,8 +619,6 @@ private void writeUtilStubs(Symbol serviceSymbol) {
writer.addImport("smithy_http", "tuples_to_fields");
writer.addImport("smithy_http.aio", "HTTPResponse", "_HTTPResponse");
writer.addImport("smithy_core.aio.utils", "async_list");
writer.addImport("smithy_core.aio.interfaces", "ClientErrorInfo");
writer.addStdlibImport("typing", "Any");

writer.write("""
class $1L($2T):
Expand All @@ -636,9 +634,7 @@ class $3L:
def __init__(self, *, client_config: HTTPClientConfiguration | None = None):
self._client_config = client_config

def get_error_info(self, exception: Exception, **kwargs: Any) -> ClientErrorInfo:
\"\"\"Get information about an exception.\"\"\"
return ClientErrorInfo(is_timeout_error=False)
TIMEOUT_EXCEPTIONS = ()

async def send(
self, request: HTTPRequest, *, request_config: HTTPRequestConfiguration | None = None
Expand All @@ -663,9 +659,7 @@ def __init__(
self.fields = tuples_to_fields(headers or [])
self.body = body

def get_error_info(self, exception: Exception, **kwargs: Any) -> ClientErrorInfo:
\"\"\"Get information about an exception.\"\"\"
return ClientErrorInfo(is_timeout_error=False)
TIMEOUT_EXCEPTIONS = ()

async def send(
self, request: HTTPRequest, *, request_config: HTTPRequestConfiguration | None = None
Expand Down
16 changes: 5 additions & 11 deletions packages/smithy-core/src/smithy_core/aio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from collections.abc import Awaitable, Callable, Sequence
from copy import copy
from dataclasses import dataclass, field, replace
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any

from .. import URI
from ..auth import AuthParams
Expand All @@ -32,7 +32,6 @@
ClientProtocol,
ClientTransport,
EndpointResolver,
ErrorClassifyingTransport,
Request,
Response,
)
Expand Down Expand Up @@ -469,15 +468,10 @@ async def _handle_attempt[I: SerializeableShape, O: DeserializeableShape](
request=request_context.transport_request
)
except Exception as e:
if hasattr(self.transport, "get_error_info"):
classifying_transport = cast(
ErrorClassifyingTransport[TRequest, TResponse], self.transport
)
error_info = classifying_transport.get_error_info(e)
if error_info.is_timeout_error:
raise ClientTimeoutError(
message=f"Client timeout occurred: {e}"
) from e
if isinstance(e, self.transport.TIMEOUT_EXCEPTIONS):
raise ClientTimeoutError(
message=f"Client timeout occurred: {e}"
) from e
raise

_LOGGER.debug("Received response: %s", transport_response)
Expand Down
38 changes: 6 additions & 32 deletions packages/smithy-core/src/smithy_core/aio/interfaces/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from collections.abc import AsyncIterable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable

from ...documents import TypeRegistry
Expand All @@ -11,15 +10,6 @@
from ...interfaces import StreamingBlob as SyncStreamingBlob
from .eventstream import EventPublisher, EventReceiver


@dataclass(frozen=True)
class ClientErrorInfo:
"""Information about an error from a transport."""

is_timeout_error: bool
"""Whether this error represents a timeout condition."""


if TYPE_CHECKING:
from typing_extensions import TypeForm

Expand Down Expand Up @@ -96,32 +86,16 @@ async def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> Endpoin


class ClientTransport[I: Request, O: Response](Protocol):
"""Protocol-agnostic representation of a client transport (e.g. an HTTP client)."""

async def send(self, request: I) -> O:
"""Send a request over the transport and receive the response."""
...

"""Protocol-agnostic representation of a client transport (e.g. an HTTP client).

class ErrorClassifyingTransport[I: Request, O: Response](
ClientTransport[I, O], Protocol
):
"""A client transport that can classify errors for retry and timeout detection.

Transport implementations should implement this protocol if they can determine
which exceptions represent timeout conditions or other classifiable error types.
Transports must define TIMEOUT_EXCEPTIONS as a tuple of exception types that
are raised when a timeout occurs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably isn't needed? We can annotate the TIMEOUT_EXCEPTIONS if it's unclear but the Protocol is already enforcing this requirement.

"""

def get_error_info(self, exception: Exception, **kwargs: Any) -> ClientErrorInfo:
"""Get information about an exception.

Args:
exception: The exception to analyze
**kwargs: Additional context for analysis
TIMEOUT_EXCEPTIONS: tuple[type[Exception], ...]

Returns:
ClientErrorInfo with error classification details.
"""
async def send(self, request: I) -> O:
"""Send a request over the transport and receive the response."""
...


Expand Down
8 changes: 2 additions & 6 deletions packages/smithy-http/src/smithy_http/aio/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
except ImportError:
HAS_AIOHTTP = False # type: ignore

from smithy_core.aio.interfaces import ClientErrorInfo, StreamingBlob
from smithy_core.aio.interfaces import StreamingBlob
from smithy_core.aio.types import AsyncBytesReader
from smithy_core.aio.utils import async_list
from smithy_core.exceptions import MissingDependencyError
Expand Down Expand Up @@ -52,11 +52,7 @@ def __post_init__(self) -> None:
class AIOHTTPClient(HTTPClient):
"""Implementation of :py:class:`.interfaces.HTTPClient` using aiohttp."""

def get_error_info(self, exception: Exception, **kwargs: Any) -> ClientErrorInfo:
if isinstance(exception, TimeoutError):
return ClientErrorInfo(is_timeout_error=True)

return ClientErrorInfo(is_timeout_error=False)
TIMEOUT_EXCEPTIONS = (TimeoutError,)

def __init__(
self,
Expand Down
39 changes: 20 additions & 19 deletions packages/smithy-http/src/smithy_http/aio/crt.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@

from smithy_core import interfaces as core_interfaces
from smithy_core.aio import interfaces as core_aio_interfaces
from smithy_core.aio.interfaces import ClientErrorInfo
from smithy_core.aio.types import AsyncBytesReader
from smithy_core.exceptions import MissingDependencyError

Expand Down Expand Up @@ -132,19 +131,16 @@ def __post_init__(self) -> None:
_assert_crt()


class _CRTTimeoutError(Exception):
"""Internal wrapper for CRT timeout errors."""


class AWSCRTHTTPClient(http_aio_interfaces.HTTPClient):
_HTTP_PORT = 80
_HTTPS_PORT = 443
_TIMEOUT_ERROR_NAMES = frozenset(["AWS_IO_SOCKET_TIMEOUT", "AWS_IO_SOCKET_CLOSED"])

def get_error_info(self, exception: Exception, **kwargs: Any) -> ClientErrorInfo:
timeout_indicators = (
"AWS_IO_SOCKET_TIMEOUT",
"AWS_IO_SOCKET_CLOSED",
)
if isinstance(exception, AwsCrtError) and exception.name in timeout_indicators:
return ClientErrorInfo(is_timeout_error=True)

return ClientErrorInfo(is_timeout_error=False)
TIMEOUT_EXCEPTIONS = (_CRTTimeoutError,)

def __init__(
self,
Expand Down Expand Up @@ -176,18 +172,23 @@ async def send(
:param request: The request including destination URI, fields, payload.
:param request_config: Configuration specific to this request.
"""
crt_request = self._marshal_request(request)
connection = await self._get_connection(request.destination)
try:
crt_request = self._marshal_request(request)
connection = await self._get_connection(request.destination)

# Convert body to async iterator for request_body_generator
body_generator = self._create_body_generator(request.body)
# Convert body to async iterator for request_body_generator
body_generator = self._create_body_generator(request.body)

crt_stream = connection.request(
crt_request,
request_body_generator=body_generator,
)
crt_stream = connection.request(
crt_request,
request_body_generator=body_generator,
)

return await self._await_response(crt_stream)
return await self._await_response(crt_stream)
except AwsCrtError as e:
if e.name in self._TIMEOUT_ERROR_NAMES:
raise _CRTTimeoutError() from e
raise

async def _await_response(
self, stream: "AIOHttpClientStreamUnified"
Expand Down
85 changes: 0 additions & 85 deletions packages/smithy-http/tests/unit/aio/test_timeout_errors.py

This file was deleted.