Summary
Any command that sends a long text field fails with HTTP 414 Request-URI Too Large, because trello.js puts every write parameter in the query string instead of the request body. card:update --description is the most visible case: the request is rejected before Trello processes it, and the card is left unchanged. With CJK text the limit is reached at roughly 900 characters, which makes this easy to run into — particularly when descriptions are generated rather than hand-typed.
Environment
- trello-cli 1.7.0
- trello.js 1.2.8
- Node.js v26.7.0
- Trello REST API v1
How to reproduce
trello card:update --board MyBoard --list ToDo --card MyCard \
--description "$(printf '這是一段中文描述。%.0s' $(seq 1 200))"
The command exits non-zero with AxiosError: Request failed with status code 414 and the card description stays empty.
The threshold is a URL of roughly 8KB. Probing PUT /1/cards/{id} with a dummy key and token is enough to find it, because the length check happens at the edge before authentication:
desc length |
Approx. URL length |
Response |
| 8,000 ASCII characters |
8,170 |
401 — passed the length check |
| 8,200 ASCII characters |
8,370 |
414 |
Percent-encoding is what makes this bite so early for non-ASCII text. A CJK character is 3 UTF-8 bytes, and each byte becomes %XX — 9 characters per glyph. So a description of about 900 Chinese characters already exceeds the limit, and a 10,014-byte description expands to 30,042 characters in the URL.
Root cause
Cards.updateCard in trello.js 1.2.8 (src/api/cards.ts in that package) places every field, including desc, in RequestConfig.params:
RequestConfig is an alias for AxiosRequestConfig, so params is serialised into the query string.
BaseClient.sendRequest only adds key and token to params before handing the config to axios. Nothing ever moves a parameter into the body, so the request body is empty.
Cards.createCard and Cards.addCardComment are built the same way, which is why --description on card:create and --text on card:comment fail identically.
This is not an oversight in trello.js — it follows Trello's own OpenAPI description. Of the 96 POST/PUT operations in swagger.v3.json, only 10 declare a requestBody, and those 10 are exactly the ones trello.js models with data. Only two operations document that a body may be used instead: POST /cards and PUT /cards/{id} ("Query parameters may also be replaced with a JSON request body instead"). The spec understates what the API accepts, and trello.js implements the spec faithfully.
Upgrading does not help. 1.2.8 is the last 1.x release, and it is what ^1.2.4 already resolves to. In the current 2.1.6, params is renamed to searchParams but desc still goes in the query string, and createCard and createCardComment are unchanged. Its core client does accept a body option and sets Content-Type: application/json for it, but the endpoint functions use that only for the same handful of operations the spec marks with requestBody. That is structural rather than an omission: 2.x is generated from Trello's swagger by a codegen, so while the spec declares these parameters in: query, every regeneration keeps them there.
Do POST and PUT endpoints accept parameters in a JSON body?
Since the spec only documents this for two operations, it was worth establishing empirically before proposing anything. Verified against the live API on a throwaway board. In every case all listed parameters were sent only in the JSON body, with the query string carrying nothing but key and token:
| Endpoint |
Parameters sent in the body |
POST /cards |
idList, name, desc, idLabels, idMembers |
PUT /cards/{id} |
desc (10,014 bytes of CJK), idLabels, closed, pos, due, dueComplete |
POST /cards/{id}/actions/comments |
text (short, and 10,014 bytes of CJK) |
POST /cards/{id}/attachments |
url, name |
POST /cards/{id}/idLabels |
value |
POST /cards/{id}/idMembers |
value |
POST /cards/{id}/checklists |
name |
PUT /cards/{id}/checkItem/{idCheckItem} |
name, state, idChecklist |
POST /checklists/{id}/checkItems |
name (required) |
POST /boards/ |
desc |
PUT /boards/{id} |
desc |
POST /boards/{id}/labels |
name, color (both required) |
PUT /labels/{id} |
name |
POST /lists |
name, idBoard (both required) |
PUT /lists/{id} |
name |
POST /lists/{id}/moveAllCards |
idBoard, idList (both required) |
All 16 returned 200 with the change applied. Only two of them — POST /cards and PUT /cards/{id} — document body support; the other 14 declare their parameters query-only and honoured the body anyway, including four endpoints whose required parameters were supplied exclusively in the body. That pattern suggests Trello merges body and query at the framework level rather than per endpoint.
Value types were checked separately on PUT /cards/{id}, because moving to a body means values are sent as native JSON rather than percent-encoded strings. JSON arrays (idLabels, idMembers), comma-joined strings, native booleans (closed, dueComplete), native numbers (pos) and ISO date strings all applied correctly. The native Date string that chrono-node produces — for example Thu Dec 31 2026 12:00:00 GMT+0800 (台北標準時間), which is what --due sends today — parsed to exactly the same instant through the body as through the query string. An explicit null is ignored rather than clearing the field.
Two endpoints could not be confirmed:
- A nested object (
coordinates on PUT /cards/{id}) returned 200 but read back as null. trello-cli never sends object-valued parameters, so nothing in the CLI depends on it.
PUT /notifications/{id} returned 401 unauthorized notification requested for both body and query string on every notification tested, so its body support is genuinely unverified rather than confirmed either way. notification:read and notification:unread use this endpoint. Because query string and body fail identically today, no regression is expected there, but it is the one gap in the coverage above.
Proposed solution
Wrap the TrelloClient instance in trello-cli so that POST and PUT parameters travel in a JSON request body:
- For
POST and PUT, move params into data. GET and DELETE are untouched.
- Skip any request that already carries a
data payload, so the multipart file upload in createCardAttachment and the endpoints trello.js models with data are left alone. (URL-based attachments leave data undefined and so do move to the body — verified above.)
- Leave
key and token in the query string. The wrapper runs before BaseClient.sendRequest, which appends the credentials afterwards, so they stay where they are.
- Drop
undefined and null values, matching what the existing query serialiser does with empty values.
This has to wrap the instance rather than subclass TrelloClient: the test suite replaces TrelloClient via jest.mock("trello.js") with a constructor that returns a plain object, which discards the subclass prototype and makes an overridden sendRequest unreachable. Wrapping the instance leaves all 28 existing test files working unchanged.
Verified end to end with the same build against the live API, using a 10,014-byte (3,338 character) CJK description: before the change card:update exits 1 with 414 and the description stays empty; after it exits 0 and stores all 3,338 characters byte-for-byte. card:comment with the same content also succeeds. The full suite passes (42 suites / 316 tests) with tsc --noEmit clean and no new eslint errors.
Two alternatives were considered and rejected:
- Fix
trello.js upstream. This is where the root cause lives, but it is a harder ask than it looks. 2.x is codegen output from Trello's swagger, so the fix has to be an override in the generator rather than an edit to a file, and the maintainer may reasonably prefer to keep matching the published spec. Even if it landed, it would not reach trello-cli until the dependency moved to 2.x, which is a separate migration: 2.1.6 is ESM-only, requires Node >= 22 (CI here tests 18.x and 20.x), and replaces the client API — client.cards.updateCard(p) becomes updateCard(client, p) — so every call site changes. The wrapper is compatible with an upstream fix either way: once trello.js sends write parameters in the body itself, the wrapper becomes a no-op for those calls, since it skips any request that already carries data.
- Patch
trello.js with pnpm patch. trello-cli is published to npm, and a patch only applies inside the development repo, so users installing from npm would not get the fix.
The cost of the wrapper is that it reaches into a dependency's request pipeline: if trello.js changes the shape of sendRequest or of RequestConfig, the wrapper needs revisiting. That is a narrow surface — one method and two fields — but it is a real coupling.
Impact on existing commands
Commands that stop failing on long text:
card:update --description
card:create --description
card:comment --text
board:create --description
- The interactive TUI: multi-line description editing (
D) and creating a card with a description
Scope of the change is wider than that list, though: 23 commands issue POST or PUT requests, and every one of them has its parameters moved to the body. The five above are the ones that were failing; the other 18 send short parameters that fit in a URL today and will behave the same, just over a different transport.
What changes on the wire:
POST and PUT parameters move from the query string to a JSON body with Content-Type: application/json.
- Values are sent as native JSON types instead of percent-encoded strings. Verified to parse identically.
- The practical length limit for a description goes from about 900 CJK characters to whatever Trello's own field limit is.
What does not change:
GET and DELETE requests.
- Requests that already carry a body, including the multipart file upload path in
createCardAttachment.
key and token, which stay in the query string.
- Every command's flags, output formats and exit codes. This is a transport-layer change with no user-facing surface.
References
An implementation is ready and a PR can reference this issue.
🤖 Generated with Claude Code
Summary
Any command that sends a long text field fails with
HTTP 414 Request-URI Too Large, becausetrello.jsputs every write parameter in the query string instead of the request body.card:update --descriptionis the most visible case: the request is rejected before Trello processes it, and the card is left unchanged. With CJK text the limit is reached at roughly 900 characters, which makes this easy to run into — particularly when descriptions are generated rather than hand-typed.Environment
How to reproduce
The command exits non-zero with
AxiosError: Request failed with status code 414and the card description stays empty.The threshold is a URL of roughly 8KB. Probing
PUT /1/cards/{id}with a dummy key and token is enough to find it, because the length check happens at the edge before authentication:desclength401— passed the length check414Percent-encoding is what makes this bite so early for non-ASCII text. A CJK character is 3 UTF-8 bytes, and each byte becomes
%XX— 9 characters per glyph. So a description of about 900 Chinese characters already exceeds the limit, and a 10,014-byte description expands to 30,042 characters in the URL.Root cause
Cards.updateCardintrello.js1.2.8 (src/api/cards.tsin that package) places every field, includingdesc, inRequestConfig.params:RequestConfigis an alias forAxiosRequestConfig, soparamsis serialised into the query string.BaseClient.sendRequestonly addskeyandtokentoparamsbefore handing the config to axios. Nothing ever moves a parameter into the body, so the request body is empty.Cards.createCardandCards.addCardCommentare built the same way, which is why--descriptiononcard:createand--textoncard:commentfail identically.This is not an oversight in
trello.js— it follows Trello's own OpenAPI description. Of the 96POST/PUToperations inswagger.v3.json, only 10 declare arequestBody, and those 10 are exactly the onestrello.jsmodels withdata. Only two operations document that a body may be used instead:POST /cardsandPUT /cards/{id}("Query parameters may also be replaced with a JSON request body instead"). The spec understates what the API accepts, andtrello.jsimplements the spec faithfully.Upgrading does not help.
1.2.8is the last1.xrelease, and it is what^1.2.4already resolves to. In the current2.1.6,paramsis renamed tosearchParamsbutdescstill goes in the query string, andcreateCardandcreateCardCommentare unchanged. Its core client does accept abodyoption and setsContent-Type: application/jsonfor it, but the endpoint functions use that only for the same handful of operations the spec marks withrequestBody. That is structural rather than an omission:2.xis generated from Trello's swagger by a codegen, so while the spec declares these parametersin: query, every regeneration keeps them there.Do POST and PUT endpoints accept parameters in a JSON body?
Since the spec only documents this for two operations, it was worth establishing empirically before proposing anything. Verified against the live API on a throwaway board. In every case all listed parameters were sent only in the JSON body, with the query string carrying nothing but
keyandtoken:POST /cardsidList,name,desc,idLabels,idMembersPUT /cards/{id}desc(10,014 bytes of CJK),idLabels,closed,pos,due,dueCompletePOST /cards/{id}/actions/commentstext(short, and 10,014 bytes of CJK)POST /cards/{id}/attachmentsurl,namePOST /cards/{id}/idLabelsvaluePOST /cards/{id}/idMembersvaluePOST /cards/{id}/checklistsnamePUT /cards/{id}/checkItem/{idCheckItem}name,state,idChecklistPOST /checklists/{id}/checkItemsname(required)POST /boards/descPUT /boards/{id}descPOST /boards/{id}/labelsname,color(both required)PUT /labels/{id}namePOST /listsname,idBoard(both required)PUT /lists/{id}namePOST /lists/{id}/moveAllCardsidBoard,idList(both required)All 16 returned
200with the change applied. Only two of them —POST /cardsandPUT /cards/{id}— document body support; the other 14 declare their parameters query-only and honoured the body anyway, including four endpoints whose required parameters were supplied exclusively in the body. That pattern suggests Trello merges body and query at the framework level rather than per endpoint.Value types were checked separately on
PUT /cards/{id}, because moving to a body means values are sent as native JSON rather than percent-encoded strings. JSON arrays (idLabels,idMembers), comma-joined strings, native booleans (closed,dueComplete), native numbers (pos) and ISO date strings all applied correctly. The nativeDatestring thatchrono-nodeproduces — for exampleThu Dec 31 2026 12:00:00 GMT+0800 (台北標準時間), which is what--duesends today — parsed to exactly the same instant through the body as through the query string. An explicitnullis ignored rather than clearing the field.Two endpoints could not be confirmed:
coordinatesonPUT /cards/{id}) returned200but read back asnull. trello-cli never sends object-valued parameters, so nothing in the CLI depends on it.PUT /notifications/{id}returned401 unauthorized notification requestedfor both body and query string on every notification tested, so its body support is genuinely unverified rather than confirmed either way.notification:readandnotification:unreaduse this endpoint. Because query string and body fail identically today, no regression is expected there, but it is the one gap in the coverage above.Proposed solution
Wrap the
TrelloClientinstance in trello-cli so thatPOSTandPUTparameters travel in a JSON request body:POSTandPUT, moveparamsintodata.GETandDELETEare untouched.datapayload, so the multipart file upload increateCardAttachmentand the endpointstrello.jsmodels withdataare left alone. (URL-based attachments leavedataundefined and so do move to the body — verified above.)keyandtokenin the query string. The wrapper runs beforeBaseClient.sendRequest, which appends the credentials afterwards, so they stay where they are.undefinedandnullvalues, matching what the existing query serialiser does with empty values.This has to wrap the instance rather than subclass
TrelloClient: the test suite replacesTrelloClientviajest.mock("trello.js")with a constructor that returns a plain object, which discards the subclass prototype and makes an overriddensendRequestunreachable. Wrapping the instance leaves all 28 existing test files working unchanged.Verified end to end with the same build against the live API, using a 10,014-byte (3,338 character) CJK description: before the change
card:updateexits 1 with414and the description stays empty; after it exits 0 and stores all 3,338 characters byte-for-byte.card:commentwith the same content also succeeds. The full suite passes (42 suites / 316 tests) withtsc --noEmitclean and no new eslint errors.Two alternatives were considered and rejected:
trello.jsupstream. This is where the root cause lives, but it is a harder ask than it looks.2.xis codegen output from Trello's swagger, so the fix has to be an override in the generator rather than an edit to a file, and the maintainer may reasonably prefer to keep matching the published spec. Even if it landed, it would not reach trello-cli until the dependency moved to2.x, which is a separate migration:2.1.6is ESM-only, requires Node >= 22 (CI here tests 18.x and 20.x), and replaces the client API —client.cards.updateCard(p)becomesupdateCard(client, p)— so every call site changes. The wrapper is compatible with an upstream fix either way: oncetrello.jssends write parameters in the body itself, the wrapper becomes a no-op for those calls, since it skips any request that already carriesdata.trello.jswithpnpm patch. trello-cli is published to npm, and a patch only applies inside the development repo, so users installing from npm would not get the fix.The cost of the wrapper is that it reaches into a dependency's request pipeline: if
trello.jschanges the shape ofsendRequestor ofRequestConfig, the wrapper needs revisiting. That is a narrow surface — one method and two fields — but it is a real coupling.Impact on existing commands
Commands that stop failing on long text:
card:update --descriptioncard:create --descriptioncard:comment --textboard:create --descriptionD) and creating a card with a descriptionScope of the change is wider than that list, though: 23 commands issue
POSTorPUTrequests, and every one of them has its parameters moved to the body. The five above are the ones that were failing; the other 18 send short parameters that fit in a URL today and will behave the same, just over a different transport.What changes on the wire:
POSTandPUTparameters move from the query string to a JSON body withContent-Type: application/json.What does not change:
GETandDELETErequests.createCardAttachment.keyandtoken, which stay in the query string.References
PUT /cards/{id}— "Query parameters may also be replaced with a JSON request body instead": https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-putPUTwith-d '{...}'andContent-Type: application/json: https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/An implementation is ready and a PR can reference this issue.
🤖 Generated with Claude Code