Calling the API: a redirect or an HTML page is handed to the agent as a successful answer #7

Closed
opened 2026-10-01 10:48:01 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo-mcp at 0b29e70b9, reading the API client and running it against a mock transport.

The client rightly does not follow redirects, but _call treats only a status of 400 or above as a failure. A 3xx comes back to the tool as {} (empty body) or as {"text": "<html>…"}, and so does a 200 that carries an HTML page instead of JSON. The agent is told nothing went wrong, so a write that never reached Postulo reads as one that did.

  • Where: src/postulo_mcp/client.py:117-131 (Postulo._call); src/postulo_mcp/client.py:50-66 (Settings.from_environment, no look at the scheme); README.md:145.
  • What happens: with a mock transport, a 301 on GET /applications returned {}; a 302 to a login page on POST /applications/1/events returned {'text': '<html>login</html>'}; a 200 with Content-Type: text/html returned {'text': '<html>maintenance</html>'}. None raised PostuloError. Two ordinary set-ups produce these answers:
    1. POSTULO_URL=http://postulo.example.org against a production instance. config/settings/prod.py:28 in the core defaults SECURE_SSL_REDIRECT to on, and /api/ is not in SECURE_REDIRECT_EXEMPT, so every call answers 301 with an empty body. list_applications returns {}, and with --write, add_note, change_status, add_reminder and create_cover_letter_draft all return {} without writing anything.
    2. An instance behind a forward-auth proxy (Authelia, Authentik, oauth2-proxy) that answers an unrecognised request with a 302 to its portal, or a 200 login page.
  • Why it matters: the agent can tell the person a note or a status change was recorded when nothing was, or conclude from {} that they have no applications. In case 1 the personal access token has also crossed the network in clear on every call before the redirect, and nothing says so; POSTULO_MCP_INSECURE, the comparable risk, is announced on stderr every time. The core met the same pattern with its health check, where curl -f took a 301 for success (#82 in the core, recorded in prod.py).
  • Fix: in _call, raise PostuloError for any status outside 2xx, naming the status and, for a 3xx, the Location header ("Postulo redirected to https://…; set POSTULO_URL to that address"). Treat a 2xx whose Content-Type is not application/json (or a +json type) as an error rather than returning {"text": …}; an empty 204 can stay {}. In Settings.from_environment, when the URL is http:// and the host is not loopback, print a warning on stderr like the one for POSTULO_MCP_INSECURE. Do not refuse it outright: the core supports plain HTTP inside a mesh VPN (prod.py, POSTULO_SECURE_COOKIES). Say this in the README's environment table.

Done when

  • tests/test_server.py has tests in which the fake answers a 301 to a GET, a 302 to a POST, and a 200 with an HTML body; each tool call ends in an error the agent can read, naming the status (and the redirect target for the 3xx), instead of returning {} or {"text": …}.
  • A test in tests/test_server.py shows that an http:// address for a non-loopback host prints a warning on stderr at start-up.
Found on 2026-09-29 in a read-only review of postulo-mcp at 0b29e70b9, reading the API client and running it against a mock transport. The client rightly does not follow redirects, but `_call` treats only a status of 400 or above as a failure. A 3xx comes back to the tool as `{}` (empty body) or as `{"text": "<html>…"}`, and so does a 200 that carries an HTML page instead of JSON. The agent is told nothing went wrong, so a write that never reached Postulo reads as one that did. - **Where:** `src/postulo_mcp/client.py:117-131` (`Postulo._call`); `src/postulo_mcp/client.py:50-66` (`Settings.from_environment`, no look at the scheme); `README.md:145`. - **What happens:** with a mock transport, a 301 on `GET /applications` returned `{}`; a 302 to a login page on `POST /applications/1/events` returned `{'text': '<html>login</html>'}`; a 200 with `Content-Type: text/html` returned `{'text': '<html>maintenance</html>'}`. None raised `PostuloError`. Two ordinary set-ups produce these answers: 1. `POSTULO_URL=http://postulo.example.org` against a production instance. `config/settings/prod.py:28` in the core defaults `SECURE_SSL_REDIRECT` to on, and `/api/` is not in `SECURE_REDIRECT_EXEMPT`, so every call answers 301 with an empty body. `list_applications` returns `{}`, and with `--write`, `add_note`, `change_status`, `add_reminder` and `create_cover_letter_draft` all return `{}` without writing anything. 2. An instance behind a forward-auth proxy (Authelia, Authentik, oauth2-proxy) that answers an unrecognised request with a 302 to its portal, or a 200 login page. - **Why it matters:** the agent can tell the person a note or a status change was recorded when nothing was, or conclude from `{}` that they have no applications. In case 1 the personal access token has also crossed the network in clear on every call before the redirect, and nothing says so; `POSTULO_MCP_INSECURE`, the comparable risk, is announced on stderr every time. The core met the same pattern with its health check, where `curl -f` took a 301 for success (#82 in the core, recorded in `prod.py`). - **Fix:** in `_call`, raise `PostuloError` for any status outside 2xx, naming the status and, for a 3xx, the `Location` header ("Postulo redirected to https://…; set POSTULO_URL to that address"). Treat a 2xx whose `Content-Type` is not `application/json` (or a `+json` type) as an error rather than returning `{"text": …}`; an empty 204 can stay `{}`. In `Settings.from_environment`, when the URL is `http://` and the host is not loopback, print a warning on stderr like the one for `POSTULO_MCP_INSECURE`. Do not refuse it outright: the core supports plain HTTP inside a mesh VPN (`prod.py`, `POSTULO_SECURE_COOKIES`). Say this in the README's environment table. ## Done when - `tests/test_server.py` has tests in which the fake answers a 301 to a GET, a 302 to a POST, and a 200 with an HTML body; each tool call ends in an error the agent can read, naming the status (and the redirect target for the 3xx), instead of returning `{}` or `{"text": …}`. - A test in `tests/test_server.py` shows that an `http://` address for a non-loopback host prints a warning on stderr at start-up.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:48:01 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo-mcp#7
No description provided.