Calling the API: a redirect or an HTML page is handed to the agent as a successful answer #7
Labels
No labels
bug
documentation
enhancement
security
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-mcp#7
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
_calltreats 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.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.GET /applicationsreturned{}; a 302 to a login page onPOST /applications/1/eventsreturned{'text': '<html>login</html>'}; a 200 withContent-Type: text/htmlreturned{'text': '<html>maintenance</html>'}. None raisedPostuloError. Two ordinary set-ups produce these answers:POSTULO_URL=http://postulo.example.orgagainst a production instance.config/settings/prod.py:28in the core defaultsSECURE_SSL_REDIRECTto on, and/api/is not inSECURE_REDIRECT_EXEMPT, so every call answers 301 with an empty body.list_applicationsreturns{}, and with--write,add_note,change_status,add_reminderandcreate_cover_letter_draftall return{}without writing anything.{}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, wherecurl -ftook a 301 for success (#82 in the core, recorded inprod.py)._call, raisePostuloErrorfor any status outside 2xx, naming the status and, for a 3xx, theLocationheader ("Postulo redirected to https://…; set POSTULO_URL to that address"). Treat a 2xx whoseContent-Typeis notapplication/json(or a+jsontype) as an error rather than returning{"text": …}; an empty 204 can stay{}. InSettings.from_environment, when the URL ishttp://and the host is not loopback, print a warning on stderr like the one forPOSTULO_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.pyhas 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": …}.tests/test_server.pyshows that anhttp://address for a non-loopback host prints a warning on stderr at start-up.Postulo._callreads every answer whole, with no size ceiling and no deadline for the whole exchange #18