fix(api): surface backend error body in upload failures#250
Conversation
The CLI's /v2/upload, S3 PUT, and prepare calls all swallowed the response body and returned only "status code N". When the backend rejects an upload with a structured JSON error (e.g. REQUIRE_PAID_PLAN for exceeding plan upload size), users saw an opaque 400 with no clue about plan limits or how to fix it. Add util.FormatHTTPError that reads the body, prefers the JSON `error` / `code` fields when present, and falls back to the raw body or status code. Apply at the six call sites across pkg/api/service.go (existing-service deploy) and internal/cmd/upload/upload.go (new project upload). After: "failed to create upload session: Upload size 53048320 bytes exceeds your plan's limit of 52428800 bytes. Upgrade your plan to upload larger files. (code: REQUIRE_PAID_PLAN, status: 400)" Refs SEI-717.
📝 WalkthroughWalkthroughThis PR introduces a reusable HTTP error formatting utility and integrates it into the upload workflow. A new 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Pull request overview
Adds a shared HTTP error formatter so upload-related API calls surface backend-provided error bodies (including structured JSON errors) instead of only reporting a status code, improving diagnosability of /v2/upload, S3 PUT, and prepare failures.
Changes:
- Introduce
util.FormatHTTPError(action, resp)to parse JSON{error, code}responses and fall back to raw body/status. - Apply the formatter to upload failure paths in both existing-service deploy and new-project upload flows.
- Add unit tests covering structured/unstructured/empty response bodies.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pkg/util/http.go | Adds FormatHTTPError helper to preserve server error bodies in CLI errors. |
| pkg/util/http_test.go | Adds unit tests for structured JSON errors and fallback formatting behavior. |
| pkg/api/service.go | Uses FormatHTTPError for non-success responses in existing-service upload flow. |
| internal/cmd/upload/upload.go | Uses FormatHTTPError for non-success responses in new-project upload flow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 67c4b429-97bf-4b15-b338-69476b4106f4
📒 Files selected for processing (4)
internal/cmd/upload/upload.gopkg/api/service.gopkg/util/http.gopkg/util/http_test.go
There was a problem hiding this comment.
Review Summary (consolidated from team discussion)
Verdict: ✅ Approve (5 approve, 1 request-changes on pre-existing code)
Actionable suggestion (optional, low priority)
- Cap
io.ReadAllwithLimitReader—pkg/util/http.go:21does an unbounded read. A misbehaving proxy could return a multi-MB HTML page. One-liner fix:body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1 MiB cap
Non-blocking observations
- The
respvariable reuse in the prepare step (bothupload.goandservice.go) is confusing but pre-existing — best addressed in the planned dedup refactor. - Structured errors use
(status: %d)while raw-body fallback uses"status code %d". Arguably intentional (metadata vs. primary message), but could be unified if consistency is preferred. pkg/api/zsend_rest.goalready has a near-identical "parse error JSON from non-2xx body" implementation with a different format. Natural convergence target once this helper lands.- Several other endpoints (
release.go,template/get/get.go,template/deploy/deploy.go,validate.go) have the same bare-status pattern — good follow-up targets forFormatHTTPError.
What we liked
- Minimal, focused fix — does exactly what it says, no scope creep.
- Graceful fallback chain: structured JSON → raw body → bare status code.
- Excellent test coverage with contract-focused assertions (including the real
REQUIRE_PAID_PLANpayload). - Exemplary PR description: real incident context, before/after, explicit scope boundaries, verification steps.
Ship it! 🚀
|
@coderabbitai review |
✅ Action performedReview finished.
|
Description
The CLI's
/v2/upload, S3 PUT, and prepare calls all swallowed the response body and returned onlystatus code N. When the backend rejects an upload with a structured JSON error (e.g.REQUIRE_PAID_PLANfor exceeding plan upload size), users saw an opaque 400 with no clue about plan limits or how to fix it.Surfaced by Discord thread
1513102734407106630: same project, same CLI, succeeded at 08:04 UTC, then started failing from 08:22 UTC after the bundle crossed the 50 MiB Free Plan limit. Took ~30 min of backend trace + Stripe + entitlement spelunking on the support side to recover a cause that was literally on the wire — backend already returned:{"code":"REQUIRE_PAID_PLAN","error":"Upload size 53048320 bytes exceeds your plan's limit of 52428800 bytes. Upgrade your plan to upload larger files.","limit":52428800,"content_length":53048320}Change
Add
util.FormatHTTPError(action string, resp *http.Response) errorwhich reads the body, prefers the JSONerror/codefields when present, and falls back to the raw body or status code. Apply at the six call sites:pkg/api/service.go(existing_servicedeploy)internal/cmd/upload/upload.go(new_projectupload)Unit-tested across structured errors (with/without
code), non-JSON bodies, empty bodies, and JSON bodies missing theerrorfield.Before / after
Before:
After:
Out of scope
The two
UploadZipToServiceimplementations (pkg/api/service.goandinternal/cmd/upload/upload.go) are near-duplicates and worth unifying, but that refactor is bigger than this fix.Related issues & labels
Verification
go build ./...cleango vet ./...cleango test ./...clean (new tests inpkg/util/http_test.go)golangci-lint run ./pkg/util/... ./pkg/api/... ./internal/cmd/upload/...0 issuesNeed help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by CodeRabbit
Bug Fixes
Tests