fix(core): surface a custom uploadEndpoint's presign error body - #364
fix(core): surface a custom uploadEndpoint's presign error body#364AminDhouib wants to merge 1 commit into
Conversation
`TokenEndpointCredentials.getPresignedUrl` threw
`Presign request failed: <status> <statusText>` without ever reading a non-ok
response, so the sentence a self-hosted token endpoint wrote for the user — a
plan-limit message, an expired-session notice — was discarded before any
handler saw it. With `onError` typed `(errorMessage: string) => void` the
thrown error's `.status` is not reachable either, which left consumers matching
the HTTP status back out of upup's own message text as the only way to recover
their own copy.
The strategy now reads the body and builds its error through
`uploadErrorFromResponse`, the helper direct-PUT, multipart, server credentials
and drive transfer already use: the body's message becomes `error.message`,
a `code` field lands on `error.code`, and `error.status` still carries the
status.
`parseErrorBody` selected its message with `error ?? msg`, so a body shaped
`{ message, error: true }` took the boolean, failed the string guard, and fell
through to the raw-JSON text fallback. It now prefers a *string* `error` and
otherwise keeps `message`.
Backward compatible by construction: same thrown class (`UpupNetworkError`
via `kind: 'network'`), and when the body is empty, whitespace or unreadable
the message stays byte-identical to the old wording. No public API change —
`onError` keeps its signature and no export surface moves.
RED before (vitest, packages/core):
FAIL tests/strategies/token-endpoint.test.ts > endpoint error body >
throws the endpoint's own message and code instead of the status line
Expected: "File exceeds your plan's 4608MB limit. Upgrade for larger uploads."
Received: "Presign request failed: 413 Payload Too Large"
FAIL ... > lifts a `message` field that sits beside a non-string `error` flag
FAIL ... > uses a plain-text error body verbatim
FAIL src/__tests__/errors.test.ts > parseErrorBody >
keeps `message` when a non-string `error` flag sits beside it
Test Files 2 failed (2)
Tests 4 failed | 59 passed (63)
GREEN after: 63 passed (63); full core suite 1699 passed (142 files);
react 644 passed, server 337 passed against a rebuilt core dist.
The three pre-existing error-path tests mock a response with no `text()` at
all and are left untouched, so they now double as the unreadable-body
compatibility pin.
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
Test evidenceRun on Node 20.20.2 ( RED — new tests against unmodified The three tests that pass in the RED run are the deliberate compatibility GREEN — same two files with the fix restored: Full gates (all via
One environment note for whoever picks this up: on a fresh clone the react |
The defect
TokenEndpointCredentials.getPresignedUrl— the strategy behind a customuploadEndpoint— never reads a non-ok response:So whatever the host application wrote into the body is discarded before any
handler can see it. Since
onErroris typed(errorMessage: string) => void,the thrown error's
.statusis not reachable either — the HTTP status survivesonly as text inside upup's own message.
That leaves a consumer whose presign endpoint enforces real rules with exactly
one way to recover its own copy: regex the status back out of our message.
Downstream evidence
Postify (usepostify.com) runs upup v3 in
uploadEndpointmode. Its/api/upload-tokenroute returns HTTP 413 with a real sentence — "File exceedsyour plan's 4608MB limit. Upgrade for larger uploads." — and the user was shown
"Presign request failed: 413 Payload Too Large" instead. The workaround it
shipped,
lib/compose/upload-error-copy.ts, is a status-string matcher:Every consumer with a rule-enforcing presign endpoint has to write some version
of that file, and it re-breaks the moment our message wording changes. This
patch is what deletes it.
The fix
The strategy now reads the body and builds its error through
uploadErrorFromResponse— the same helper direct-PUT, multipart, servercredentials and drive transfer already use. The body's message becomes
error.message(which is whatonErrorreceives), acodefield lands onerror.code, anderror.statusstill carries the HTTP status.One supporting fix in
parseErrorBody: it selected its message witherror ?? msg, so a body shaped{ message: "...", error: true }— common inhand-rolled endpoints — took the boolean, failed the
typeof === 'string'guard, and fell all the way through to the raw-JSON text fallback. It now
prefers a string
errorand otherwise keepsmessage.Compatibility
Deliberately no public-API change —
onErrorkeeps its(errorMessage: string) => voidsignature, and nothing is added to any package'sexport surface.
kind: 'network'buildsUpupNetworkError, exactlywhat this path threw before, still carrying
.status.whitespace, or unreadable, the message stays
Presign request failed: <status> <statusText>— the wording this path hasalways thrown. A consumer matching the old string sees no change on that path.
(This is why the fallback is explicit rather than letting
uploadErrorFromResponsefall back to its own bare"<status> <statusText>".)has that body surface instead of being dropped. That is the bug.
parseErrorBody's change only affects bodies whereerroris present and nota string — which previously produced a raw JSON dump, never something a
consumer could have been relying on.
state.uploadErrorCode(rendered byFileListinreact and angular today), so a
codefrom the body is usable for branchingwithout touching prose.
Tests
packages/core/tests/strategies/token-endpoint.test.ts— a newendpoint error bodyblock covering: the endpoint's message + code replacingthe status line, the
{ message, error: true }shape, a plain-text body, theclass/status still being
UpupNetworkError/413, and both legacy-wordingfallbacks (empty body,
text()rejecting).The three pre-existing error-path tests mock a response with no
text()atall; they are left exactly as they were, so they now double as the
unreadable-body backward-compatibility pin.
packages/core/src/__tests__/errors.test.tsgains theparseErrorBodycase.RED-before / GREEN-after evidence is in a comment below.
Docs + changeset
apps/landing/content/docs/api-reference/error-codes.mdxgains a paragraphunder Errors from your own endpoints — that section previously listed the four
strategies routing through
uploadErrorFromResponseand the custom presign callwas not one of them.
A
patchchangeset is included per repo convention.Not a release
No release is cut by this PR and none should be inferred from it. No version
bump, no tag, no
npm publish— the changeset only queues an entry for wheneveryou decide to run the release cycle. Merging this to
devdoes not publishanything.
Targeted at
devrather thanmasterper CLAUDE.md's "Do not merge, PR, orpush to
masterwithout an explicit maintainer decision."