fix(cli): match feedback attachment limit to the Vercel body cap and keep oversized logs' tails

Merged
KI
2mo ago
+387-32

Problem

superset feedback submit checked attachments against a 10 MB total, mirroring the server's 14M-char zod refine in packages/trpc/src/router/support/support.ts. That refine is not the real limit: the tRPC route is a Vercel function, and Vercel rejects request bodies above 4.5 MB with 413 FUNCTION_PAYLOAD_TOO_LARGE before the route runs. A user's 5.1 MB host-service.log passed the CLI check today and failed on upload, and their agent had to hand-truncate the file.

Change

  • One budget in the CLI, derived from the 4.5 MB body cap minus 200 KB headroom for the report (up to 20k chars), title, JSON framing, and the diagnostics bundle (under 90k chars encoded). The raw-byte limit comes from it through the 4/3 base64 expansion explicitly (4 chars per 3 bytes), landing at 3.2 MB of attachment bytes.
  • A lone file over the budget is attached as its tail (the newest bytes) instead of failing, with one stderr line: Truncated host-service.log to its last 3225000 bytes (3.2 MB attachment limit). Logs are the common case and their tail is what matters.
  • Several files that together exceed the budget fail before anything uploads, naming the files and the limit.
  • Only the bytes that will be sent are read, through the same tail reader the diagnostics bundle already used.

The limit stays in the CLI only. The CLI imports @superset/trpc type-only, so a shared runtime constant would pull server code into the CLI bundle. The server refine is untouched.

Testing

Co-located command.test.ts covers the four cases: the budget fits under 4.5 MB after base64 expansion, an under-limit file passes through byte-identical, a single oversized file arrives as exactly its last 3.2 MB with the stderr line, and two files that only fit individually fail before the mutation is called with both names and the limit in the message.

Proof the tests catch the bug: running the test file against the pre-fix command.ts (with only export added to its two constants so the module loads) fails 3 of 4: the budget test sees 14,020,000 chars against the 4.5 MB cap, the truncation test gets the whole file, and the multi-file test gets the old generic "Attachments exceed the 10MB total limit" without filenames.

Against production, without auth, so nothing was submitted: a 413 means Vercel's cap fired before tRPC ran, a 401 means the body got through.

Request bodyResult
4,320,109 bytes (3,225,000 raw attachment + 20k report)401, reached tRPC
4,409,765 bytes (worst case the fixed CLI can send: full attachment, maximal diagnostics bundle, 20k report, 200-char title/os/filename)401, reached tRPC
4,493,409 bytes401, reached tRPC (largest passing, by bisection)
4,494,107 bytes413 FUNCTION_PAYLOAD_TOO_LARGE (smallest failing)
4,520,109 bytes (the earlier 3.3 MB variant with 100 KB headroom, plus a 20k report)413 FUNCTION_PAYLOAD_TOO_LARGE

So the platform cap is decimal 4.5 MB minus about 6 KB, not 4.5 MiB, and the 200 KB headroom is what keeps the worst-case body under it. The 3.3 MB variant this PR briefly carried would have failed with a full report attached.

  • bun test src/commands/feedback/submit in packages/cli: 4 pass, 0 fail
  • bun run typecheck in packages/cli: clean
  • bunx biome check packages/cli/src/commands/feedback/submit: clean

Follow-up: surface server rejections plainly

Reviewer ask: when the server rejects the submission, show the status and the server's message, plus the attachment-limit hint on a 413.

What a 413 looked like before this: Vercel answers with a text body, the tRPC client fails to parse it and throws "Failed to parse JSON" with no status and no response attached, so the command could not recover anything useful.

  • packages/cli/src/lib/api-client.ts now passes a fetch wrapper to httpBatchLink. A non-2xx response whose body is not JSON becomes an ApiHttpError with the status, status text, and the body's non-empty lines joined (Vercel's includes the request id, useful for support). tRPC keeps a thrown fetch error as the cause of its own error, verified empirically. JSON error responses are left to tRPC as before. This is the one change outside the feedback command directory; the status is not recoverable anywhere else.
  • The feedback command catches any rejection with an HTTP status behind it, tRPC's data.httpStatus or the wrapper's, and throws The server rejected the feedback (HTTP 413): Request Entity Too Large / FUNCTION_PAYLOAD_TOO_LARGE / sfo1::..., with the hint The request was over the API's 4.5 MB body cap. Attachments must total under 3.2 MB; ... on a 413. UNAUTHORIZED and network errors still pass through so the runner's "Session expired" and "Could not connect to API" wording is unchanged.
  • The readTailBytes helper also now honors the byte count readSync returns, so a log rotated between the size check and the read is not padded with zero bytes (a bot review point that was valid).

Tests: api-client.test.ts drives the wrapper against a local Bun.serve for a 413 text body, a 400 JSON body, and a 200. command.test.ts adds three cases: a 413 surfaces its status, the FUNCTION_PAYLOAD_TOO_LARGE text, the request id, and the 3.2 MB hint; a tRPC 400 surfaces its status and message with no hint; an UNAUTHORIZED error is rethrown untouched. Against the pre-fix command, the two rejection tests fail with TRPCClientError where CLIError is expected.

  • bun test src/commands/feedback/submit src/lib/api-client.test.ts in packages/cli: 10 pass, 0 fail (full package suite: 205 pass)
  • bun run typecheck in packages/cli: clean
  • bunx biome check on the four changed files: clean

Evidence: built binaries, before and after

Two CLI binaries built with cli-framework build, one from main's command.ts and api-client.ts and one from this branch, both with the API URL baked to a local server that answers every request the way Vercel's edge does for an oversized body (413, text body Request Entity Too Large / FUNCTION_PAYLOAD_TOO_LARGE / sfo1::evidence-0001). Same 6.8 MB log attached to both.

=== BEFORE (main) ===
Error: Failed to parse JSON
exit=1

=== AFTER (PR #7214) ===
Truncated host-service.log to its last 3225000 bytes (3.2 MB attachment limit)
Error: The server rejected the feedback (HTTP 413): Request Entity Too Large / FUNCTION_PAYLOAD_TOO_LARGE / sfo1::evidence-0001
Hint: The request was over the API's 4.5 MB body cap. Attachments must total under 3.2 MB; attach fewer or smaller files, or a single log so its tail is kept
exit=1

The real production 413 through the real client: createApiClient with a bogus bearer and a 4.6 MB body against api.superset.sh (nothing can be submitted with a bad token) throws a TRPCClientError whose cause is ApiHttpError with status 413 and body Request Entity Too Large / FUNCTION_PAYLOAD_TOO_LARGE / sfo1::zjm9r-1788668505990-9a08386ce624.

Side observation, not changed here: production answers an invalid sk_live_ API key with a 500 "Invalid API key" tRPC error rather than a 401, so the built binary with a fake key printed The server rejected the feedback (HTTP 500): Invalid API key. That is the new tRPC-error path working, but the server's status code for that case looks wrong.

https://claude.ai/code/session_01RnHTYgESRFnZbhHRM4nsPE
https://claude.ai/code/session_01UWyngsh3WSdEM6kmf1P8s5

Summary by CodeRabbit

  • New Features

    • Feedback attachments now account for base64-encoded request sizes.
    • Large individual attachments are truncated to their newest content, with warnings and truncation details included in the response.
    • Attachment diagnostics share the same size budget.
    • Smaller attachments continue to upload intact.
  • Bug Fixes

    • Oversized attachment sets are rejected before upload, with affected files identified.
    • Submission errors now preserve relevant status and server messages, including guidance for payloads that exceed the request limit.