This repository was archived by the owner on Oct 26, 2026. It is now read-only.
Repository navigation
Commit 1f8ef0a
fix(fields): report a failed ImageField upload instead of leaking the rejection (#10310)
Fixes #10226
Clause-②: no
## What changed
`ImageField` (`@object-ui/fields`) has two upload paths: the native
picker (`handleFileChange`) and the crop dialog's confirm
(`handleCropConfirm`). On `main` at `8b1f066` both ran the upload inside
`try … finally` with **no `catch`**. Both handlers are fire-and-forget:
React ignores the promise an `onChange` handler returns, and
`ImageCropperDialog` calls `onConfirm(blob, name)` without awaiting it.
So a rejected upload (network error, 413, storage outage) showed nothing
and escaped as an unhandled promise rejection.
This PR catches the failure on both paths and renders the same message
`FileField` renders for the same failure through the same `useUpload()`
transport: key `fields.file.uploadFailed`, same `name` / `error`
arguments, same `defaultValue`. The message goes in the widget's
existing error row (the one the oversize-pick rejections already use,
objectui#4141).
- **Picker path:** each pick is caught on its own, as in `FileField`'s
`useFileUploads`. A failed pick is reported and not added. In a
multi-select, the picks that did upload still land, and the oversize
rejections are kept beside the upload failures.
- **Crop path:** the original image stays (no `onChange`). The dialog
still closes in `finally`, so the message shows in the field's error row
instead of behind the dialog.
- The message is a module-level helper, `uploadFailedMessage(t, name,
err)`, in the same shape as `maxSizeError`. A component-scoped
`useCallback` made the React Compiler skip one more memoization, so the
helper lives outside the component.
The key exists in every locale pack `@object-ui/i18n` ships.
`all-locales-key-parity.test.ts` re-derives that, and `pnpm
check:i18n-keys` checks the call site's arguments and inline
`defaultValue` against the `en` pack.
Files: `packages/fields/src/widgets/ImageField.tsx`,
`packages/fields/src/widgets/ImageField.uploadFailure.test.tsx` (new),
`.changeset/10226-image-upload-failure-reported.md` (patch,
`@object-ui/fields`).
## Evidence (head `737663e`)
**Red first, on unmodified source.** I ran the new test against
`8b1f066` before touching the widget. All three failure cases went red,
and each saw exactly one `unhandledRejection` carrying `network down`,
with no failure text in the DOM:
```
× picker: shows the translated failure, adds nothing, leaks no rejection
× picker (multiple): adds the successful picks and reports only the failed one
× crop: shows the translated failure, keeps the original image, leaks no rejection
AssertionError: expected [ Error: network down ] to have a length of +0 but got 1 (x3)
TestingLibraryElementError: Unable to find an element with the text: Failed to upload "bad.png": network down
Tests 3 failed | 2 passed (5)
```
The two success controls (picker and crop, resolving transport) pass on
both trees.
**Ablation on the committed fix (head `737663e`).** A trap-guarded
script replaced `ImageField.tsx` with its `8b1f066` blob, ran the test,
and restored the file with `git checkout HEAD -- PATH`.
- On-disk proof of the mutation: `uploadFailed` occurrences went 4 → 0
and `catch (err)` went to 0.
- Result on the mutated tree: `Tests 3 failed | 2 passed (5)`, the same
three failures as above.
- Restore proof: the file's hash equals its HEAD blob, and `git diff
HEAD` is empty.
**Local gates, on head `737663e`, exit codes written to files before
being read:**
- `pnpm --filter @object-ui/fields type-check` (`tsc --noEmit && tsc -p
tsconfig.test.json`) → exit 0. I built the dependency closure first with
`pnpm --filter '@object-ui/fields^...' build`.
- `pnpm exec vitest run packages/fields/` → exit 0, `Test Files 179
passed | 1 skipped (180)`, `Tests 3010 passed | 7 skipped (3017)`.
- `eslint` on the two touched `.ts(x)` files → 0 errors.
`ImageField.tsx` has 7 warnings against 6 on `8b1f066`; the one new
warning is below. `lint.yml` sets no `--max-warnings`.
- `check:i18n-keys`, `check:i18n-dead-keys`, `check:vi-mock-specifiers`,
`check:vi-mock-inherit`, `check:vi-mock-override-shape`,
`check:new-line-citations`, `check:control-bytes`,
`check:test-path-roots`, `check:unreferenced-sources`,
`scripts/check-changeset-presence.mjs`,
`scripts/check-changeset-no-major.mjs` → all exit 0.
**Scope of local runs:** repo-wide `pnpm lint`, the full `pnpm test`,
and every other `check:*` family were left to CI. I ran lint only on the
touched files. That narrowing is not a proof that no other file's
verdict can move, and I did not check whether type-aware rules are on in
`eslint.config.js`.
## Acceptance notes
- **One new lint warning, not fixed here.**
`react-hooks/preserve-manual-memoization` now also fires on
`handleCropConfirm`. Adding the `catch` makes the React Compiler skip
memoizing that callback; it already skipped `openCropper` on `main`.
This only affects optimization, and the file already carries the same
warning. `lint.yml` sets no `--max-warnings`.
- **Non-`Error` rejections read `undefined`.** For a rejection that is
not an `Error`, `(err as Error).message` puts `undefined` in the
message. `FileField` does exactly the same, and I copied its behaviour
on purpose so the two widgets say the same thing. Carrier: none.
- **Possible shared helper.** `FileField` and `ImageField` now each
build the `fields.file.uploadFailed` call the same way.
`file-size-guard.ts` explains why the size guard is shared rather than
copied per widget, and the same argument could apply to this message.
Moving it would touch `FileField.tsx`, which is outside this claim's
file surface. Carrier: none.
- **Title strings in the crop dialog are hard-coded English.**
`ImageCropperDialog`'s title and description do not go through `t()`. I
left them alone: they are not part of this defect and not in scope.
- **Commit trailer.** The first commit (`2091538`) has a
`Co-Authored-By` trailer that names a model version. The later commits
use the plain `Claude` form. History was not rewritten.
Session: `https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb`
---
_Generated by [Claude
Code](https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Leehom <pm@objectstack.ai>1 parent 687353f commit 1f8ef0a
3 files changed
Lines changed: 228 additions & 8 deletions
File tree
- .changeset
- packages/fields/src/widgets
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
26 | 42 | | |
27 | 43 | | |
28 | 44 | | |
| |||
47 | 63 | | |
48 | 64 | | |
49 | 65 | | |
50 | | - | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
51 | 70 | | |
52 | 71 | | |
53 | 72 | | |
| |||
97 | 116 | | |
98 | 117 | | |
99 | 118 | | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
100 | 123 | | |
101 | 124 | | |
102 | 125 | | |
| |||
176 | 199 | | |
177 | 200 | | |
178 | 201 | | |
| 202 | + | |
179 | 203 | | |
180 | 204 | | |
181 | 205 | | |
182 | 206 | | |
183 | 207 | | |
184 | 208 | | |
185 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
186 | 212 | | |
187 | | - | |
188 | | - | |
189 | | - | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
190 | 221 | | |
191 | 222 | | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
192 | 226 | | |
193 | 227 | | |
194 | 228 | | |
| |||
287 | 321 | | |
288 | 322 | | |
289 | 323 | | |
290 | | - | |
291 | | - | |
292 | | - | |
| 324 | + | |
| 325 | + | |
293 | 326 | | |
294 | 327 | | |
295 | 328 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
0 commit comments