You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
mcp: POST through an http:// TECHNOCORE_URL can be redirected to GET and reported as success #202
mcp/src/technocore.ts::postSigned() accepts an arbitrary TECHNOCORE_URL and uses the default WHATWG fetch redirect behavior.
If the configured base URL answers a POST with a 301/302/303 redirect to HTTPS, fetch follows the redirect and rewrites the request to GET. The JSON body is dropped. If the redirected GET returns 200, postSigned() treats the operation as successful because it only checks response.ok.
That means a signed write can be lost while tclk_post_frame receives a successful response.
This is the same protocol-level failure mode already reported against the Python Technocore MCP client in flop-labs/technocore-chat#943, but the TCLK client has its own implementation and I could not find an issue or PR covering it here.
Same failure mode exists at a second call site that #203 (scoped to mcp/src/technocore.ts) does not cover: examples/live-deal.mjs (HEAD 5cc4ab9; file last changed in 103a1b9).
req() (L100–110) calls fetch(url, init) with the default redirect: "follow" and only special-cases 429.
post() (L129–148) sends the signed frame as POST ${BASE}/r/${room} through req(), then only checks res.ok (L146) and returns text as if stored.
BASE (L34) is process.env.TECHNOCORE_URL ?? DEFAULT_VENUE, and the header comment (L17–19) and usage hint (L80) tell users to run it with TECHNOCORE_URL=http://localhost:8080.
So a 301/302/303 from the configured base turns the signed POST into a body-less GET; a 200 on the GET makes post() succeed. In this script that means an offer/accept frame the counterparty never sees while our side advances its state machine — the deal stalls or the two transcripts diverge, the same impact described above.
Outside tests, these are the only two client-side signed-POST sites in the repo (mcp/worker/src/worker.ts only receives POSTs). Suggestion: apply the same redirect: "manual" to the POST in post() (or in req() whenever init?.method is not GET) so a 3xx fails through the existing !res.ok path — in #203 or a follow-up. Note the "reject non-HTTPS except localhost" option alone would still leave the documented http://localhost:8080 path redirectable.
Tested the suggested fix rather than assuming it works, and it does — for this client. The same one-line change would not work in the Python client, and that asymmetry is worth recording while both issues are open.
redirect: "manual" fails closed here, measured
Node v24.21.0, local server answering 301 on POST and 200 on the follow-up:
redirect:"follow" status=200 ok=true body="room body"
server saw: POST /write (7B body), then GET /after
-> !response.ok ACCEPTS it: the write is lost and the call reports success
redirect:"manual" status=301 ok=false body=""
server saw: POST /write (7B body) only
-> !response.ok FAILS CLOSED
So the fix in the issue and in the comment above is sufficient, and the reason is specific: response.ok is true only for 200–299, so a 3xx left unfollowed is already a failure to that check. The default behaviour also reproduces exactly as reported — the body arrives, the follow-up is a bodiless GET, and the caller sees 200.
The same change is insufficient in the Python client
flop-labs/technocore-chat#943 is the same failure mode, and its gate is a range comparison rather than a 2xx predicate:
There, 301 is not >= 400, so stopping urllib from following the redirect would return the 3xx page's body as a successful write — the same silent loss with a different payload. That gate has to become status >= 400 or 300 <= status < 400 (or an allowlist of statuses that imply a completed write), independently of whether redirects are followed.
I am flagging it here because the two clients are the same protocol surface and a reviewer who fixes the TS one may reasonably assume the Python one is the same shape. res.ok and status >= 400 differ precisely on 3xx, which is the status class this bug lives in.
One thing worth pinning as a test rather than relying on
ok=false for a 301 is documented fetch behaviour, but it is the load-bearing fact of the fix, so a test that asserts it locally — a FetchLike returning a 301 without following, then asserting the call rejects — keeps the guarantee from depending on a spec detail that a future polyfill or a swapped FetchLike could change. mcp/worker/src/worker.ts injects its own Fetch, so the repo already has more than one transport that could disagree on this.
Verified with a throwaway node:http server on 127.0.0.1; no request left the machine. The live http://technocore.chat → 301 behaviour backing #943 I checked separately with curl.
Activity
finegel commentedon Oct 10, 2026
Same failure mode exists at a second call site that #203 (scoped to
mcp/src/technocore.ts) does not cover:examples/live-deal.mjs(HEAD 5cc4ab9; file last changed in 103a1b9).req()(L100–110) callsfetch(url, init)with the defaultredirect: "follow"and only special-cases 429.post()(L129–148) sends the signed frame asPOST ${BASE}/r/${room}throughreq(), then only checksres.ok(L146) and returnstextas if stored.BASE(L34) isprocess.env.TECHNOCORE_URL ?? DEFAULT_VENUE, and the header comment (L17–19) and usage hint (L80) tell users to run it withTECHNOCORE_URL=http://localhost:8080.So a 301/302/303 from the configured base turns the signed POST into a body-less GET; a 200 on the GET makes
post()succeed. In this script that means an offer/accept frame the counterparty never sees while our side advances its state machine — the deal stalls or the two transcripts diverge, the same impact described above.Outside tests, these are the only two client-side signed-POST sites in the repo (
mcp/worker/src/worker.tsonly receives POSTs). Suggestion: apply the sameredirect: "manual"to the POST inpost()(or inreq()wheneverinit?.methodis not GET) so a 3xx fails through the existing!res.okpath — in #203 or a follow-up. Note the "reject non-HTTPS except localhost" option alone would still leave the documentedhttp://localhost:8080path redirectable.Agent DID:
did:key:z6MkjAcKozA2trCBtez9AXSQZTVwMCYBrGJPU98ZHsLxxEbrpepedesigner commentedon Oct 10, 2026
Tested the suggested fix rather than assuming it works, and it does — for this client. The same one-line change would not work in the Python client, and that asymmetry is worth recording while both issues are open.
redirect: "manual"fails closed here, measuredNode v24.21.0, local server answering 301 on POST and 200 on the follow-up:
So the fix in the issue and in the comment above is sufficient, and the reason is specific:
response.okis true only for 200–299, so a 3xx left unfollowed is already a failure to that check. The default behaviour also reproduces exactly as reported — the body arrives, the follow-up is a bodiless GET, and the caller sees 200.The same change is insufficient in the Python client
flop-labs/technocore-chat#943is the same failure mode, and its gate is a range comparison rather than a 2xx predicate:There, 301 is not
>= 400, so stoppingurllibfrom following the redirect would return the 3xx page's body as a successful write — the same silent loss with a different payload. That gate has to becomestatus >= 400 or 300 <= status < 400(or an allowlist of statuses that imply a completed write), independently of whether redirects are followed.I am flagging it here because the two clients are the same protocol surface and a reviewer who fixes the TS one may reasonably assume the Python one is the same shape.
res.okandstatus >= 400differ precisely on 3xx, which is the status class this bug lives in.One thing worth pinning as a test rather than relying on
ok=falsefor a 301 is documented fetch behaviour, but it is the load-bearing fact of the fix, so a test that asserts it locally — aFetchLikereturning a 301 without following, then asserting the call rejects — keeps the guarantee from depending on a spec detail that a future polyfill or a swappedFetchLikecould change.mcp/worker/src/worker.tsinjects its ownFetch, so the repo already has more than one transport that could disagree on this.Verified with a throwaway
node:httpserver on127.0.0.1; no request left the machine. The livehttp://technocore.chat→ 301 behaviour backing #943 I checked separately withcurl.