Repository navigation
Review 14 Oct: Cloudinary refresh race, weclapp properties hint, Odoo expired key hint, SMTP in compose - #1084
Merged
Merged
Conversation
…nnector id, lock renewals Cloudinary's MCP server issues 5-minute access tokens and rotates the refresh token on every renewal. Tool calls through /mcp reached the OAuth2 token service without the connector id, and parsed a fresh authConfig on every call. So nothing was cached, the renewed token was never stored, and the next renewal replayed the spent refresh token: invalid_grant a few minutes after each authorization. The same path served REST and GraphQL tool calls. - The tool-call path passes the connector id to the engines (except for credentials built from caller-context variables). - A renewal for a saved connector runs under a Postgres advisory lock held by a transaction, with the reads and the write of the rotated token on that transaction, so the two backends of a blue/green deploy never spend the same refresh token. A renewal that waited takes the token the other process stored. If the write does not survive the transaction, it is written again outside it. - The MCP bridge recognises the SDK's 401 (status in error.data, the message holds only the body), so the renewal after a rejection runs.
…ud token help weclapp: about 50 list calls in ten days failed with 400 "unknown property", all guessed names in `properties`. Each list tool now says to leave it empty and lists real field names from weclapp's API v2 OpenAPI, with the names guessed wrong in production and what to use instead. The error hint first says to retry without `properties`. Odoo: since Odoo 18 API keys expire and the key dialog preselects "1 Day", so "Invalid apikey" (JSON-2) or Access Denied (JSON-RPC) the next day means an expired key. Setup steps, field help, the verify hint and a runtime hint say to create a key with Duration "Persistent Key" (administrators) or the longest duration offered. Odoo JSON-2 404: the database-name lookup asked /json/2/web/database/list and never found anything; it now asks the origin. Before offering the JSON-RPC connector, the check asks Odoo's common.version: below 19 it offers JSON-RPC, on 19 it says the database name is wrong and names the right one, and on Odoo Online's "typo" page it says the address is not an Odoo database. SAP S/4HANA Cloud: the instructions gave a BTP token URL while the field help gave the tenant API host. Both now point to the Token Service URL shown under the arrangement's OAuth 2.0 Details, on the -api host. A 401 lists the four usual causes (token URL, client of another communication user, missing arrangement, scope); a 403 names the missing arrangement.
docker-compose.yml and the quickstart listed the app's environment one variable at a time and left out SMTP_*, so the SMTP settings setup.sh writes into .env never reached the app. Both now pass SMTP_HOST, PORT, USER, PASS, FROM, FROM_NAME, REPLY_TO and SECURE. The cloud compose already did. SMTP_FROM_NAME, which setup.sh and railway.json ask for, was never read. It now names the sender when SMTP_FROM is a bare address. A test checks that every SMTP_* variable the email service reads is passed by both compose files.
A urlencoded or formdata body was mapped field by field without a bodyEncoding, and Content-Type is not a replayed header, so the engine sent the fields as JSON (the HAR importer had the same bug, fixed in #1057). urlencoded bodies now get bodyEncoding form-urlencoded. formdata bodies get form-data (multipart), and a file field becomes a __file marker the engine fills from the public URL the caller passes, as the Etsy image upload does.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes from the 14 Oct review, plus three Odoo/SAP items from two stuck Odoo 19 users and the Postman form-body bug that #1057 fixed for HAR.
1. Cloudinary: OAuth refresh failed minutes after each authorization (our bug)
Root cause. Tool calls through
/mcpbuilt the engine config withoutconnectorId, for every engine type, and parsed a freshauthConfigfrom the registry's JSON string on every call. As a result,OAuth2TokenService:Cloudinary's MCP server issues 5-minute access tokens (
expires_in: 300) and rotates the refresh token. The first renewal after five minutes succeeded and its new refresh token was dropped. The next renewal replayed the spent one and gotinvalid_grant: Invalid or expired refresh token.The cloud logs show it:
/mcpreadtoken refresh failed:with nofor connector ….cmv6t4iry…was re-authorized at 06:11:43. Its 06:32 call renewed successfully, yet the row was never updated: nolastRefreshedAt, andupdated_atis still 06:11:43.The "two refreshes 1 s apart" were two Test connection clicks after the token was already dead. #916 had fixed Test connection and discovery, not the tool-call path.
Fix.
connectorIdto the engines. Credentials built from{{amcp.*}}caller variables stay unkeyed, because they differ per caller.pg_advisory_xact_lock(hashtext('amcp:oauth2-refresh:<id>'))).SdkHttpErrorcarries the status indata.status, and its message is only the body (Error POSTing to endpoint: {"error":"invalid_token"…}), so the oldmessage.includes('401')check never fired and the renewal after a rejection never ran.Tests.
oauth2-token.service.refresh-race.spec.tsuses a rotating fake provider and a shared fake table with advisory-lock semantics:authConfigper call;The SQL was checked against a real Postgres 16 with two Prisma clients.
Existing connectors.
cmv6t4iry043u01k3iuqwrbpoandcmv6r9xu100hr01k3zn3ui7vtwere re-authorized this morning but will break again within minutes of their next renewal until this is deployed. Rotating OAuth providers on REST or GraphQL tool calls (JTL, Sage, DATEV) were exposed to the same path.2. weclapp: 400 "unknown property"
About 50 failures over 10 workspaces since 4 Oct. All of them were guessed names in
properties, such ascustomerNumberon orders and invoices,openAmount,salesOrderItems,shippingAddress,unitName,stockQuantityandvalidUntil.propertiesnow starts with "Leave empty (the default)". It then lists real top-level field names from weclapp's API v2 OpenAPI (https://www.weclapp.com/api/openapi_v2.yaml), with the names guessed wrong in production and what to use instead.propertieson large lists.properties.3. Odoo: "Invalid apikey" after a day = expired key
Since Odoo 18, API keys expire, and the New API Key dialog preselects 1 Day (the first entry of
_selection_duration). Persistent Key is offered to administrators only; other users get at most their group'sapi_key_duration, which is 90 days for internal users.In the cloud: 63 JSON-2
Invalid apikey401s across 5 workspaces, and about 40 JSON-RPCAccess Deniedacross 9 workspaces, in two weeks. Changes, forodooandodoo-jsonrpc:Invalid apikeyandOdoo Server Error: Access Denied.They say the key has likely expired and to create a new one with Duration "Persistent Key" or the longest duration offered.
Odoo JSON-2 404 (added on request):
/json/2/web/database/list, which 404s, so it never suggested anything for the JSON-2 adapter. It now asks the origin.common.version:odoo.com/typo?domain=…(checked live). In that case the check says the address is not an Odoo database.saas~19versions are parsed. If the version cannot be read, the old hint and suggestion stay.4. SAP S/4HANA Cloud token URL (added on request)
The instructions gave a BTP XSUAA token URL (
…authentication.eu10.hana.ondemand.com/oauth/token), while the field help gave the tenant-apihost. Both now point to the Token Service URL shown under the arrangement's Inbound Communication → OAuth 2.0 Details, onhttps://<tenant>-api.s4hana.cloud.sap/sap/bc/sec/oauth2/token. The base URL example now includes-apitoo.A 401 at verify or at runtime lists the four causes:
A 403 names the missing arrangement.
5. SMTP in the self-hosted compose files (#1083)
docker-compose.ymlanddocker-compose.quickstart.ymllist the app environment one variable at a time and left outSMTP_*, so whatsetup.shwrites into.envnever reached the app. Both now passSMTP_HOST/PORT/USER/PASS/FROM/FROM_NAME/REPLY_TO/SECURE.The cloud compose already passed them (Mailgun, unchanged).
SMTP_FROM_NAME, whichsetup.shandrailway.jsonask for, was never read. It now names the sender whenSMTP_FROMis a bare address. ASMTP_FROMthat already has a name wins, so the cloud is unaffected.A test checks that every
SMTP_*the email service reads is passed by both compose files.docker compose configwas run on both.6. Postman import: form bodies sent as JSON
The same bug as the HAR importer (#1057):
urlencodedbodies now getbodyEncoding: form-urlencoded.formdatabodies getform-data(multipart). A file field becomes a{ "__file": "$field" }marker that the engine fills from the public URL the caller passes, like Etsy's image upload.__proto__,constructorandprototypeare skipped, and form fields are merged in one step instead of keyed writes. This came from CodeQL's remote-property-injection rule on the changed lines.Checks
tscclean for backend and frontend; ESLint clean on all changed files.node scripts/validate-adapters.mjs: 405/405.node scripts/adapter-count.mjs --check: OK.