Skip to content

Setting a long description or comment fails with HTTP 414 Request-URI Too Large #241

Description

@chenghung

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions