Summary
studio push (and the desktop Push) fails for any site whose sync archive exceeds ~3.5 MB. The WordPress.com upload endpoint
POST https://public-api.wordpress.com/rest/v1.1/studio-file-uploads/<SITE_ID>
returns 413 Request Entity Too Large on the very first TUS creation request — before a single byte of the archive is uploaded. Studio does not report this usefully: the CLI prints the raw tus: error, and the desktop app collapses it to "Unknown error" / "Studio was unable to connect to WordPress.com."
I isolated this fairly thoroughly (details below). The 413 is driven purely by the declared Upload-Length header — it is independent of archive contents, file types, site, plan, and request rate. A ~3.5 MB ceiling is far too small for a feature that syncs whole WordPress sites, so this looks like a server-side regression/misconfiguration on the studio-file-uploads endpoint, compounded by poor client-side error handling.
Environment
- Studio 1.11.0
- Reproduced via both the real
studio push and standalone curl (so it's server-side / platform-independent)
- Account has 15 sites (Free, and Business/Atomic) — all show the identical cap
Expected vs. actual
- Expected: A 106 MB site archive uploads and imports (this worked previously).
- Actual:
413 at TUS upload creation; push never starts. Error shown to the user is non-actionable.
Real CLI output:
✔ Site exported successfully
✖ Push failed: tus: unexpected response while creating upload, originated from request
(method: POST, url: https://public-api.wordpress.com/rest/v1.1/studio-file-uploads/<SITE_ID>,
response code: 413, response text: , request id: n/a)
Steps to reproduce
- Have a local site whose sync archive is larger than ~3.5 MB (i.e. essentially any real site).
studio push --remote-site <SITE_ID> --options all
- Export succeeds; upload fails immediately with
413.
Root cause (isolated)
The endpoint rejects the creation request based solely on the declared Upload-Length. Replaying just the TUS creation POST with an empty body (no file data sent), varying only Upload-Length:
Declared Upload-Length |
Response |
| 1 KB … 3,000,000 B |
201 Created |
| 3,529,728 B |
201 Created ← last accepted |
| ~3,600,000 B and up (4 MB, 10 MB, 50 MB, 106 MB, 5 GB) |
413 |
So the cap sits at ≈3.5 MB, it is deterministic, and it is evaluated after authentication (a request with no token returns 403, not 413).
Minimal standalone repro (no Studio needed)
TOKEN="<accessToken from ~/.studio/shared.json -> authToken.accessToken>"
SITE="<numeric wpcom site id>"
META="filename $(printf push.tar.gz | base64),filetype $(printf application/gzip | base64)"
URL="https://public-api.wordpress.com/rest/v1.1/studio-file-uploads/$SITE"
# 3 MB declared -> 201 Created
curl -s -o /dev/null -w "3MB -> %{http_code}\n" -X POST "$URL" \
-H "Authorization: Bearer $TOKEN" -H "Tus-Resumable: 1.0.0" \
-H "Upload-Metadata: $META" -H "Upload-Length: 3000000" -H "Content-Length: 0"
# 106 MB declared -> 413 (empty body either way)
curl -s -o /dev/null -w "106MB -> %{http_code}\n" -X POST "$URL" \
-H "Authorization: Bearer $TOKEN" -H "Tus-Resumable: 1.0.0" \
-H "Upload-Metadata: $META" -H "Upload-Length: 111014960" -H "Content-Length: 0"
What I ruled out
| Hypothesis |
Test |
Result |
| Site/archive genuinely too large |
Site allows 2 GB media uploads, 53 GB storage free; archive is 106 MB |
Not a real limit |
| Bundling/export step |
Export produces a valid archive; 413 is the next step |
Bundling succeeds |
| File extension / name / directory |
No file-type logic exists in the push/export path |
N/A |
A specific file (e.g. a .webp) |
A real .webp inside a 0.24 MB bundle uploads fully (201 → 204, media-id returned) |
Content-blind |
| Site / plan / Atomic specific |
Probed all 15 sites on the account (Free + Business/Atomic) |
Identical ~3.5 MB cap everywhere |
| Rate limiting |
Burst of 24 interleaved requests in ~14 s: every large → 413, every small → 201, no 429, no Retry-After/X-RateLimit-*; also fails on the very first request |
Not rate-limited |
The 413 response has an empty body, Server: nginx, routed via the Automattic edge (Server-Timing: a8c-cdn …), and the endpoint is VideoPress-backed (x-videopress-upload-* in Access-Control-Expose-Headers).
Why this is hard to diagnose in Studio (client-side issues)
References are to current trunk:
- The client's size pre-check is ~1,400× too high to ever warn.
SYNC_PUSH_SIZE_LIMIT_GB = 5 (tools/common/lib/sync/constants.ts) is checked in apps/cli/commands/push.ts and the desktop slice, but the server caps at ~3.5 MB — so the pre-check always passes and the user gets no guidance.
- A 413 is retried as if it were a transient network error. In
tools/common/lib/sync/tus-upload.ts, onShouldRetry returns status !== 403, so a 413 is retried across all retryDelays and (if the upload had started) surfaces as a "network paused" event. 413 is not retryable — the payload won't shrink.
- The desktop app discards the status code.
apps/studio/src/modules/sync/lib/ipc-handlers.ts parses the error with z.object({ error: z.string() }).safeParse(error); a bare 413 (empty body / tus error object) fails that parse → returns { success: false, error: 'Unknown error' }, and getErrorFromResponse (apps/studio/src/stores/sync/sync-operations-slice.ts) ultimately shows "Studio was unable to connect to WordPress.com." The user never learns it was a 413 or that size was the trigger.
Suggested fixes
Server-side (likely needs routing to the WordPress.com sync / studio-file-uploads team):
- Raise/correct the
studio-file-uploads upload-size limit. ~3.5 MB makes Push unusable for essentially any real site. (Was this recently changed? Pushes of ~100 MB worked previously.)
Client-side (this repo):
- In
onShouldRetry, treat 413 (and other non-retryable 4xx) as terminal — stop retrying and don't show a "network" message.
- When an HTTP error has no JSON
{ error } body, fall back to reporting the status code (and a friendly explanation for 413 specifically), instead of "Unknown error" / "unable to connect."
- Align or remove the
SYNC_PUSH_SIZE_LIMIT_BYTES pre-check so it reflects the server's actual limit, and warn the user before exporting a multi-MB archive that will be rejected.
Notes
- Happy to provide additional captures (full headers, the webp-upload test, the per-site sweep) if useful.
Summary
studio push(and the desktop Push) fails for any site whose sync archive exceeds ~3.5 MB. The WordPress.com upload endpointreturns
413 Request Entity Too Largeon the very first TUS creation request — before a single byte of the archive is uploaded. Studio does not report this usefully: the CLI prints the rawtus:error, and the desktop app collapses it to "Unknown error" / "Studio was unable to connect to WordPress.com."I isolated this fairly thoroughly (details below). The 413 is driven purely by the declared
Upload-Lengthheader — it is independent of archive contents, file types, site, plan, and request rate. A ~3.5 MB ceiling is far too small for a feature that syncs whole WordPress sites, so this looks like a server-side regression/misconfiguration on thestudio-file-uploadsendpoint, compounded by poor client-side error handling.Environment
studio pushand standalonecurl(so it's server-side / platform-independent)Expected vs. actual
413at TUS upload creation; push never starts. Error shown to the user is non-actionable.Real CLI output:
Steps to reproduce
studio push --remote-site <SITE_ID> --options all413.Root cause (isolated)
The endpoint rejects the creation request based solely on the declared
Upload-Length. Replaying just the TUS creation POST with an empty body (no file data sent), varying onlyUpload-Length:Upload-LengthSo the cap sits at ≈3.5 MB, it is deterministic, and it is evaluated after authentication (a request with no token returns
403, not413).Minimal standalone repro (no Studio needed)
What I ruled out
.webp).webpinside a 0.24 MB bundle uploads fully (201 → 204, media-id returned)429, noRetry-After/X-RateLimit-*; also fails on the very first requestThe
413response has an empty body,Server: nginx, routed via the Automattic edge (Server-Timing: a8c-cdn …), and the endpoint is VideoPress-backed (x-videopress-upload-*inAccess-Control-Expose-Headers).Why this is hard to diagnose in Studio (client-side issues)
References are to current
trunk:SYNC_PUSH_SIZE_LIMIT_GB = 5(tools/common/lib/sync/constants.ts) is checked inapps/cli/commands/push.tsand the desktop slice, but the server caps at ~3.5 MB — so the pre-check always passes and the user gets no guidance.tools/common/lib/sync/tus-upload.ts,onShouldRetryreturnsstatus !== 403, so a413is retried across allretryDelaysand (if the upload had started) surfaces as a "network paused" event. 413 is not retryable — the payload won't shrink.apps/studio/src/modules/sync/lib/ipc-handlers.tsparses the error withz.object({ error: z.string() }).safeParse(error); a bare 413 (empty body /tuserror object) fails that parse → returns{ success: false, error: 'Unknown error' }, andgetErrorFromResponse(apps/studio/src/stores/sync/sync-operations-slice.ts) ultimately shows "Studio was unable to connect to WordPress.com." The user never learns it was a 413 or that size was the trigger.Suggested fixes
Server-side (likely needs routing to the WordPress.com sync /
studio-file-uploadsteam):studio-file-uploadsupload-size limit. ~3.5 MB makes Push unusable for essentially any real site. (Was this recently changed? Pushes of ~100 MB worked previously.)Client-side (this repo):
onShouldRetry, treat413(and other non-retryable 4xx) as terminal — stop retrying and don't show a "network" message.{ error }body, fall back to reporting the status code (and a friendly explanation for 413 specifically), instead of "Unknown error" / "unable to connect."SYNC_PUSH_SIZE_LIMIT_BYTESpre-check so it reflects the server's actual limit, and warn the user before exporting a multi-MB archive that will be rejected.Notes