Skip to content

default_retry_on inverts requests error handling: 4xx are retried, connection errors and timeouts are not #8802

Description

@nazsats

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangGraph rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangGraph.
  • This is not related to the langchain-community package.
  • I posted a self-contained, minimal, reproducible example.

Related Issues / PRs

Reproduction Steps / Example Code (Python)

import requests
from requests.models import Response
from typing_extensions import TypedDict

from langgraph.graph import START, StateGraph
from langgraph.types import RetryPolicy


class State(TypedDict):
    n: int


def make_graph(exc_factory):
    attempts = {"count": 0}

    def node(state: State) -> State:
        attempts["count"] += 1
        raise exc_factory()

    g = StateGraph(State)
    g.add_node(
        "call", node, retry_policy=RetryPolicy(initial_interval=0.01, max_attempts=3)
    )
    g.add_edge(START, "call")
    return g.compile(), attempts


def http_404():
    r = Response()
    r.status_code = 404
    return requests.HTTPError("404 Not Found", response=r)


app, attempts = make_graph(http_404)
try:
    app.invoke({"n": 0})
except Exception:
    pass
print(f"requests.HTTPError(404)  -> node ran {attempts['count']} time(s); expected 1")

app, attempts = make_graph(lambda: requests.ConnectionError("connection refused"))
try:
    app.invoke({"n": 0})
except Exception:
    pass
print(f"requests.ConnectionError -> node ran {attempts['count']} time(s); expected 3")

Output:

requests.HTTPError(404)  -> node ran 3 time(s); expected 1
requests.ConnectionError -> node ran 1 time(s); expected 3

Description

default_retry_on — the default value of RetryPolicy.retry_on — misclassifies requests
exceptions, and the two errors are exact inverses of each other: permanent failures are
retried, and transient failures are not.

1. Every requests.HTTPError is retried, including 4xx.

return 500 <= exc.response.status_code < 600 if exc.response else True

requests.Response.__bool__ is an alias for Response.ok, so every error response is falsy:

>>> r = requests.models.Response(); r.status_code = 404
>>> bool(r)
False

The if exc.response guard therefore always takes the else True branch, which makes the
500 <= status < 600 comparison unreachable. A 401, 404 or 422 is retried up to
max_attempts with backoff, re-sending a request that cannot succeed against an API that has
already refused it.

2. requests connection errors and timeouts are never retried.

requests.RequestException subclasses OSError, which is in the non-retryable list, so
requests.ConnectionError, ConnectTimeout and ReadTimeout are treated as permanent — even
though the builtin ConnectionError on the first line of the function retries, and an
unrecognised exception falls through to return True.

>>> isinstance(requests.exceptions.ConnectionError(), ConnectionError)  # builtin
False
>>> isinstance(requests.exceptions.ConnectionError(), OSError)
True

Expected: 4xx is not retried; 5xx, connection errors and timeouts are.
Actual: 4xx is retried; connection errors and timeouts are not.

Note on test coverage. test_should_retry_default_retry_on already asserts the intended
behaviour, but builds the response with Mock(). A bare Mock is truthy, so the test reaches
a code path production never reaches, and passes.

I have a fix with regression tests (using a real requests.models.Response) — 9 new test cases fail
on main and pass with the change, with no regressions. The branch is pushed and was opened as
#8801, which the bot closed for not linking an approved issue. Happy to relink it here once a
maintainer approves and assigns this.

System Info

System Information
------------------
> OS:  Linux
> OS Version:  #30~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug  7 13:27:52 UTC 2
> Python Version:  3.12.3 (main, Jun 19 2026, 12:46:00) [GCC 13.3.0]

Package Information
-------------------
> langgraph: 1.2.11 (main @ c0a13bb)
> langchain_core: 1.6.1
> langsmith: 0.12.1
> requests: 2.31.0
> httpx: 0.28.1

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions