Skip to content

Recursively extract HTTP errors from nested ExceptionGroups#3556

Open
mshsheikh wants to merge 1 commit into
openai:mainfrom
mshsheikh:patch-41
Open

Recursively extract HTTP errors from nested ExceptionGroups#3556
mshsheikh wants to merge 1 commit into
openai:mainfrom
mshsheikh:patch-41

Conversation

@mshsheikh
Copy link
Copy Markdown
Contributor

@mshsheikh mshsheikh commented Jun 1, 2026

This PR improves how the code finds HTTP-related errors inside nested exception groups.

Right now, _extract_http_error_from_exception() only checks the first level of BaseExceptionGroup.exceptions. That works when the HTTP error is directly inside the group, but it fails when the error is wrapped inside another ExceptionGroup. In async code, this can happen when multiple tasks fail at the same time and the errors are grouped together more than once.

With this change, the method now checks exception groups recursively. That means it keeps looking inside nested groups until it finds a matching HTTP error or confirms that none is present. This makes error detection more reliable and helps the code report the real network problem instead of missing it.

This is a small but important reliability fix. It does not change the normal success path. It only improves how nested errors are handled when failures happen.

Before

def _extract_http_error_from_exception(self, e: BaseException) -> Exception | None:
    """Extract HTTP error from exception or ExceptionGroup."""
    if isinstance(e, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException):
        return e

    # Check if it's an ExceptionGroup containing HTTP errors
    if isinstance(e, BaseExceptionGroup):
        for exc in e.exceptions:
            if isinstance(
                exc, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException
            ):
                return exc

    return None

After

def _extract_http_error_from_exception(self, e: BaseException) -> Exception | None:
    """Extract HTTP error from exception or ExceptionGroup recursively."""
    if isinstance(e, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException):
        return e

    # Recursively check ExceptionGroups for HTTP errors
    if isinstance(e, BaseExceptionGroup):
        for exc in e.exceptions:
            result = self._extract_http_error_from_exception(exc)
            if result is not None:
                return result

    return None

Why this matters

  • It catches HTTP errors even when they are nested inside multiple exception groups.
  • It improves error reporting during async cleanup and grouped task failures.
  • It helps the retry and user error logic work with the actual root cause.
  • It makes the method more robust in real async failure cases.

Impact

This change is safe and low risk. It only affects exception handling when failures occur. Normal requests and successful flows are unchanged.

Testing idea

A good test would create a nested ExceptionGroup that contains an httpx.ConnectError or httpx.TimeoutException several levels deep, then verify that the method still finds and returns the correct HTTP error.

This PR improves how the code finds HTTP-related errors inside nested exception groups.

Right now, `_extract_http_error_from_exception()` only checks the first level of `BaseExceptionGroup.exceptions`. That works when the HTTP error is directly inside the group, but it fails when the error is wrapped inside another `ExceptionGroup`. In async code, this can happen when multiple tasks fail at the same time and the errors are grouped together more than once.

With this change, the method now checks exception groups recursively. That means it keeps looking inside nested groups until it finds a matching HTTP error or confirms that none is present. This makes error detection more reliable and helps the code report the real network problem instead of missing it.

This is a small but important reliability fix. It does not change the normal success path. It only improves how nested errors are handled when failures happen.

### Before

```python
def _extract_http_error_from_exception(self, e: BaseException) -> Exception | None:
    """Extract HTTP error from exception or ExceptionGroup."""
    if isinstance(e, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException):
        return e

    # Check if it's an ExceptionGroup containing HTTP errors
    if isinstance(e, BaseExceptionGroup):
        for exc in e.exceptions:
            if isinstance(
                exc, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException
            ):
                return exc

    return None
```

### After

```python
def _extract_http_error_from_exception(self, e: BaseException) -> Exception | None:
    """Extract HTTP error from exception or ExceptionGroup recursively."""
    if isinstance(e, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException):
        return e

    if isinstance(e, BaseExceptionGroup):
        for exc in e.exceptions:
            result = self._extract_http_error_from_exception(exc)
            if result is not None:
                return result

    return None
```

### Why this matters

* It catches HTTP errors even when they are nested inside multiple exception groups.
* It improves error reporting during async cleanup and grouped task failures.
* It helps the retry and user error logic work with the actual root cause.
* It makes the method more robust in real async failure cases.

### Impact

This change is safe and low risk. It only affects exception handling when failures occur. Normal requests and successful flows are unchanged.

### Testing idea

A good test would create a nested `ExceptionGroup` that contains an `httpx.ConnectError` or `httpx.TimeoutException` several levels deep, then verify that the method still finds and returns the correct HTTP error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant