API problem documents: detail is mostly untranslated and never in the account's language; instance ignores the request id #393
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#393
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 at
95348908d, reading everyHttpErrorandRefusedraise inapi/,api/problems.pyand the middleware against the wiki's What a refusal looks like.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atapi/api.py:620(the translated capture refusal, cited below as:618) andapi/api.py:500-506(the comment about capture notifications, cited below as:498-504); every other location has not moved.Related to #296 (closed), which made refusals RFC 9457 problem documents, said "
detailstays translated", and leftinstanceas the path "if request ids arrive later"; and to #233 (closed), which added those request ids. This issue adds what #296 left untrue: mostdetailsentences never pass throughgettext, none is in the account's language on a token call, andinstancestill ignores the id that now exists.The code and the wiki describe a contract for the refusal envelope that the code keeps only in part. A client author is told to show
detailto people because it is in their language, and gets a translated sentence from a few calls and English from the rest. An operator handed a problem document has an address but nothing that leads to the log lines.1. Most
detailsentences are English literalsapi/problems.py:24-26(detail"goes throughgettextlike every other message here") and in thedetailrow of the member table in the wiki,The-capture-API.md:495("Translated into the account's language, because a person debugging reads it"). The 23 raises that pass a literal or an f-string:api/auth.py:92-104,ScopedAuth.authenticate: both insufficient-scope sentences, including the one the wiki shows as its example (The-capture-API.md:484).api/routers/common.py:22,:53,:58,:67(choice_or_422, the referral and agency checks, the priority check).api/routers/applications.py:144-146.api/routers/documents.py:127,:163.api/routers/interviews.py:59,:105,:113,:116,:147,:166.api/routers/listings.py:86,:123,:150,:155,:160,:162.api/routers/reminders.py:44,:81.api/problems.py:244-261,_validation_sentence, is English on purpose and says so in its own docstring ("Untranslated, like the field names it quotes"), which contradicts the module docstring above it.api/api.py:618,api/idempotency.py:35-36, the too-large refusal atapi/problems.py:202,api/routers/applications.py:168and the rate limit. The browser extension (src/lib/api.js:104and:117-118in postulo-chromium) and the MCP server (src/postulo_mcp/client.py:125in postulo-mcp) passdetailon to the person. "Nothing resembling a job posting was found there." arrives translated; "No such application.", "'ends_at' must be after 'starts_at'." and the scope refusal arrive in English, depending only on which line refused.2. The language is the request's, not the account's
core/middleware.py:175-181,UserPreferencesMiddleware._profile, returnsNoneunlessrequest.useris authenticated. A bearer-token call has no signed-in user (api/auth.py:64: "Nothing here logs the caller in"), and the token is checked inside the view, after the middleware has run. The owner's profile language is therefore never activated, andLocaleMiddleware's choice fromAccept-Languagestands. The comment atapi/api.py:498-504says the same about capture notifications.Accept-Languagenames; a script or the MCP server usually sends none, so it receives English. The wiki's "the account's language" is true of no refusal.tests/test_api_problems.py:281-295,test_the_title_does_not_move_with_the_language, says in its docstring "Only one is translated" but asserts onlytitle. The refusal it provokes is the insufficient-scope f-string, so the test cannot notice either gap.3.
instanceignores the request id that now existsapi/problems.py:31-35: "Nothing in Postulo mints one. … when request ids arrive, this is the line that changes."document()putsrequest.pathininstance(api/problems.py:141-142).The-capture-API.md:496: "Not a unique identifier for this occurrence — Postulo does not mint one".core/middleware.py:184-210,RequestIDMiddleware(#233): setsrequest.request_id, scopes every log line to it and returns it inX-Request-ID. It is second inMIDDLEWARE(config/settings/base.py:90), so it has run before any API view.problems.pygave for changinginstancehas been met since #233, and both texts still say it has not. The id that would find the matching log lines is only in a response header, which a person reporting a refusal rarely copies; the body they do copy carries only the path.Consequence and fix
instancehides a debugging aid the instance already has.detailis, then make code, docstring and wiki agree. Do not leave a mixture.detailis for people (what both texts say today): wrap every literal listed in section 1 ingettext, with placeholders for the values (_("'%(field)s' must be one of %(choices)s; got %(value)r.")), keeping field names and enum values literal inside the sentence, and settle_validation_sentenceone way or the other. For the language, either activate the token owner's profile language once the token is authenticated (inScopedAuth.authenticate, with the same "still offered" checkUserPreferencesMiddlewareapplies to a session), or correct the wiki to saydetailfollows the request'sAccept-Language.detailis for developers: say so inapi/problems.pyand the wiki, and stop translating the few that are, so a client sees one language.problems.document, either asinstance(for exampleurn:postulo:request:<id>, keeping the path in a separate member) or as arequest_idextension member beside the path. Update the module docstring, theProblemschema (api/problems.py:100) and the wiki's member table.Done when
tests/test_api_problems.pyhas a test asserting that a refusal's body carries the same id as itsX-Request-IDheader. It fails today.tests/test_api_problems.pyhas a test that provokes two router refusals (an interview whoseends_atprecedesstarts_at, and a token lacking thewritescope) withAccept-Language: frand assertsdetailis not the English sentence; or, ifdetailis declared English, asserts that every refusal'sdetailis the same whateverAccept-Languagesays. Either form fails today.tests/test_api_problems.pyhas a test that sends a token whose owner's profile language ispt-PTwithAccept-Language: enand pins which of the two languagesdetailis in.api/problems.pyand The capture API on the wiki describe the behaviour those tests pin, including which language a token-authenticated call gets and whatinstanceholds.X-Request-IDis dropped #976