Skip to content

fix(agent-server): enforce the paginated search limit - #4991

Merged
enyst merged 7 commits into
OpenHands:mainfrom
mkuri:fix/search-limit-validation
Sep 15, 2026
Merged

enyst merged 7 commits into
OpenHands:mainfrom
mkuri:fix/search-limit-validation

Conversation

@mkuri

@mkuri mkuri commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

HUMAN:
This PR proposes a fix for a typo in endpoint definitions (le instead of lte), which will allow to enforce limits for search queries


AGENT:

Why

The four paginated search endpoints declare their page-size bound as Query(gt=0, lte=100).

lte is not a FastAPI/Pydantic constraint keyword — the correct spelling is le — and an unknown key does not raise. It rides along into the JSON schema and is never enforced, so limit=101 passes validation and reaches the handler's bare assert limit <= 100, which surfaces as an unhandled AssertionError.

Two consequences:

  • The cap is invisible in the OpenAPI document, so a generated client (including clients/typescript) cannot see it.
  • A client error looks like a server fault: HTTP 500 instead of 422, which misleads monitoring and retry logic downstream.

Found while building an external scheduler on top of the Agent Server: paging events with limit=200 returned a 500 with no indication that the request was at fault.

Summary

  • lte=100 → le=100 on limit in conversation_router, event_router, bash_router and file_router.
  • Drop the now-redundant assert limit > 0 / assert limit <= 100 from those four handlers — FastAPI rejects both cases with 422 before the handler runs.
  • Add tests/agent_server/test_search_limit_validation.py, covering all four endpoints: the bound must be published in the schema, and out-of-range values must be 422 rather than an exception.
  • Update test_search_conversations_limit_validation, which asserted the old behaviour (pytest.raises(AssertionError) for limit=101).

The bound itself is unchanged, so every request that worked before still works. The only behaviour change is that an oversized limit now returns 422 instead of 500.

Issue Number

Fixes #5041.

How to Test

Start the agent server from this branch and call each search endpoint:

uv run python -m openhands.agent_server --host 127.0.0.1 --port 38010

CID=$(curl -s -X POST http://127.0.0.1:38010/api/conversations \
  -H 'Content-Type: application/json' \
  -d '{"workspace":{"kind":"LocalWorkspace","working_dir":"/tmp"},
       "agent":{"kind":"ACPAgent","acp_server":"claude-code",
                "acp_command":["npx","-y","--prefer-offline","@agentclientprotocol/claude-agent-acp@0.63.0"],
                "acp_model":"sonnet"}}' | jq -r .id)

for L in 100 101 500 0; do
  curl -s -o /dev/null -w "limit=$L -> %{http_code}\n" \
    "http://127.0.0.1:38010/api/conversations/$CID/events/search?limit=$L"
done

Before (released 1.47.0, same request):

limit=100 -> 200
limit=101 -> 500 {"detail":"Internal Server Error","exception":"","error_id":"d8b49d3b34e1418f8ad4903416d7acba"}

After (this branch, live server, not a test double):

events/search limit=100  -> 200 {"items":[],"next_page_id":null}
events/search limit=101  -> 422 {"detail":[{"type":"less_than_equal","loc":["query","limit"],"msg":"Input should be less than or equal to 100","input":"101","ctx":{"le":100}}]}
events/search limit=500  -> 422 {"detail":[{"type":"less_than_equal","loc":["query","limit"],"msg":"Input should be less than or equal to 100","input":"500","ctx":{"le":100}}]}
events/search limit=0    -> 422 {"detail":[{"type":"greater_than","loc":["query","limit"],"msg":"Input should be greater than 0","input":"0","ctx":{"gt":0}}]}

The other three endpoints on the same running server:

conversations/search   limit=100 -> 200   limit=101 -> 422
bash_events/search     limit=100 -> 200   limit=101 -> 422
file/search_subdirs    limit=100 -> 200   limit=101 -> 422

And the published schema now carries the bound on all four:

/api/conversations/search:                        {"type":"integer","maximum":100,"exclusiveMinimum":0,...}
/api/conversations/{conversation_id}/events/search:{"type":"integer","maximum":100,"exclusiveMinimum":0,...}
/api/bash/bash_events/search:                      {"type":"integer","maximum":100,"exclusiveMinimum":0,...}
/api/file/search_subdirs:                          {"type":"integer","maximum":100,"exclusiveMinimum":0,...}

Unit tests:

uv run pytest tests/agent_server/test_search_limit_validation.py \
              tests/agent_server/test_conversation_router.py \
              tests/agent_server/test_event_router.py \
              tests/agent_server/test_file_router.py \
              tests/agent_server/test_bash_service.py
# 215 passed

The new test file fails on main (5 of 7 cases) and passes here, so it does catch the regression it describes.

make format, make lint and uv run pre-commit run --files <changed files> are clean. The one pre-commit hook that fails — check-forbidden-dynamic-attributes, flagging stream_context.py and llm/utils/telemetry.py — fails identically on an unmodified checkout of main and is unrelated to this change.

Video/Screenshots

Not applicable — no GUI surface; the terminal output above is the evidence.

Design Doc

Not applicable: a one-keyword bug fix, which CONTRIBUTING.md lists as the kind of trivial PR where a design doc is noise.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

Behaviour is additive for well-behaved clients: the accepted range is unchanged, the schema simply becomes truthful. Any caller currently relying on a 500 for an oversized limit would now get a 422, which is the documented meaning of the response.

A repo-wide grep found no other lte=/gte= misuse in Query(...); the remaining timestamp__gte= occurrences are keyword argument names, not constraints.

mkuri and others added 2 commits September 12, 2026 21:47
…rting

The four paginated search endpoints declared their page-size bound as
`Query(gt=0, lte=100)`. `lte` is not a FastAPI/Pydantic constraint keyword
(`le` is) and an unknown key does not raise — it rides along into the JSON
schema and is never enforced. `limit=101` therefore passed validation and
reached a bare `assert limit <= 100` in the handler, which surfaced as an
unhandled AssertionError: HTTP 500 for what is a client error.

Two consequences: the cap never appeared in the OpenAPI document, so a
generated client could not see it, and exceeding it looked like a server
fault rather than a 422.

Use `le=100` and drop the now-redundant asserts. The bound is unchanged, so
requests that worked before still work; oversized ones now get 422 instead
of 500.

An existing test asserted the AssertionError, so it is updated to expect
the 422.

Affected: conversations/search, conversations/{id}/events/search,
bash/bash_events/search, file/search_subdirs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@enyst enyst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@mkuri thank you for the contribution! Looks good to me actually, sorry for reading through when it’s still a draft! Do you want it out of draft? You may want to fill in the “HUMAN:” section as well with your short description or whatever you’d like to tell :)

@enyst
enyst marked this pull request as ready for review September 14, 2026 14:16
@all-hands-bot

all-hands-bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

@enyst enyst changed the title fix(agent-server): enforce the paginated search limit instead of asserting fix(agent-server): enforce the paginated search limit Sep 14, 2026
…imit test

The search_subdirs route lives on file_discovery_router, not file_router.
The test previously imported file_router, so /api/file/search_subdirs was
absent from its OpenAPI schema and the schema assertion raised KeyError.

Co-authored-by: openhands <openhands@all-hands.dev>

enyst commented Sep 14, 2026

Copy link
Copy Markdown
Member

Fixed the failing agent-server-tests check.

Root cause: the new test test_search_limit_validation.py looked up /api/file/search_subdirs in the OpenAPI schema built from file_router, but that route is actually registered on a separate router, file_discovery_router (both share prefix="/file"). Since search_subdirs isnt on file_router, the schema lookup raised KeyError: /api/file/search_subdirs.

Fix: import file_discovery_router (instead of file_router) and use it in the SEARCH_ROUTES tuple. The route is the same; only the router object referenced by the test was wrong.

agent-server-tests is green again. ✅


This comment was created by an AI agent (OpenHands) on behalf of the user.

@enyst enyst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you!

@enyst
enyst merged commit b054a2f into OpenHands:main Sep 15, 2026
42 checks passed
Bowbee pushed a commit to PlayCastDotIo/openhands-software-agent-sdk that referenced this pull request Sep 15, 2026
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Bowbee added a commit to PlayCastDotIo/openhands-software-agent-sdk that referenced this pull request Sep 15, 2026
…uter

Upstream OpenHands#4991's test targets file_discovery_router, a discovery surface the
fork does not have; the fork serves /file/search_subdirs from file_router.

Co-authored-by: openhands <openhands@all-hands.dev>
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.

[Bug]: Oversized search limits return HTTP 500 instead of validation errors

3 participants