* ♻️ Handle fetch-error gracefully with toast instead of full-page error
Network-level failures (lost connectivity, DNS failure, etc.) on RPC
calls were propagating as :internal/:fetch-error to the global error
handler, which replaced the entire UI with a full-page error screen.
Now the :internal handler distinguishes :fetch-error from other internal
errors and shows a non-intrusive toast notification instead, allowing
the user to continue working.
* ✨ Add automatic retry with backoff for idempotent RPC requests
Idempotent (GET) RPC requests are now automatically retried up to 3
times with exponential back-off (1s, 2s, 4s) when a transient error
occurs. Retryable errors include: network-level failures
(:fetch-error), 502 Bad Gateway, 503 Service Unavailable, and browser
offline (status 0).
Mutation (POST) requests are never retried to avoid unintended
side-effects. Non-transient errors (4xx client errors, auth errors,
validation errors) propagate immediately without retry.
* ♻️ Make retry helpers public with configurable parameters
Make retryable-error? and with-retry public functions, and replace
private constants with a default-retry-config map. with-retry now
accepts an optional config map (:max-retries, :base-delay-ms) enabling
callers and tests to customize retry behavior.
* ✨ Add tests for RPC retry mechanism
Comprehensive tests for the retry helpers in app.main.repo:
- retryable-error? predicate: covers all retryable types (fetch-error,
bad-gateway, service-unavailable, offline) and non-retryable types
(validation, authentication, authorization, plain errors)
- with-retry observable wrapper: verifies immediate success, recovery
after transient failures, max-retries exhaustion, no retry for
non-retryable errors, fetch-error retry, custom config, and mixed
error scenarios
* ♻️ Introduce :network error type for fetch-level failures
Replace the awkward {:type :internal :code :fetch-error} combination
with a proper {:type :network} type in app.util.http/fetch. This makes
the error taxonomy self-explanatory and removes the special-case branch
in the :internal handler.
Consequences:
- http.cljs: emit {:type :network} instead of {:type :internal :code :fetch-error}
- errors.cljs: add a dedicated ptk/handle-error :network method (toast);
restore :internal handler to its original unconditional full-page error form
- repo.cljs: simplify retryable-types and retryable-error? — :network
replaces the former :internal special-case, no code check needed
- repo_test.cljs: update tests to use {:type :network}
* 📚 Add comment explaining the use of bit-shift-left
* 🐛 Fix TypeError when token error map lacks :error/fn key
Guard against missing :error/fn in token form control resolve streams.
When schema validation errors are produced they may not carry an
:error/fn key; calling nil as a function caused a TypeError crash.
Apply an if-let guard at all 7 affected sites across input.cljs,
color_input.cljs and fonts_combobox.cljs, falling back to :message
or returning the error map unchanged.
* ♻️ Extract token error helpers and add unit tests
Extract resolve-error-message and resolve-error-assoc-message helpers
into errors.cljs, replacing the seven duplicated inline lambdas in
input.cljs, color_input.cljs and fonts_combobox.cljs with named
function references. Add frontend-tests.tokens.token-errors-test
covering both helpers for the normal path (:error/fn present) and the
fallback path (schema-validation errors that lack :error/fn).
Signed-off-by: Penpot Dev <dev@penpot.app>
---------
Signed-off-by: Penpot Dev <dev@penpot.app>
* ✨ Improve error handling and exception formatting
- Enhance exception formatting with visual separators and cause chaining
- Add new handler for :internal error type
- Refine error types: change assertion-related errors to :assertion type
- Improve error messages and hints consistency
- Clean up error handling in zip utilities and HTTP modules
* 🐛 Properly handle AbortError on fetch request unsubscription
When a fetch request in-flight is cancelled due to RxJS unsubscription
(e.g. navigating away from the workspace while thumbnail loads are
pending), the AbortController.abort() call triggers a catch handler
that previously relied solely on a @unsubscribed? flag to suppress the
error.
This was unreliable: nested observables spawned inside rx/mapcat (such
as datauri->blob-uri conversions within get-file-object-thumbnails)
could abort independently, with their own AbortController instances,
meaning the outer unsubscribed? flag was never set and the AbortError
propagated as an unhandled exception.
Add an explicit AbortError name check as a disjunctive condition so
that abort errors originating from any observable in the chain are
suppressed at the source, regardless of subscription state.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ✨ Improve error handling and exception formatting
- Enhance exception formatting with visual separators and cause chaining
- Add new handler for :internal error type
- Refine error types: change assertion-related errors to :assertion type
- Improve error messages and hints consistency
- Clean up error handling in zip utilities and HTTP modules
* 🐛 Properly handle AbortError on fetch request unsubscription
When a fetch request in-flight is cancelled due to RxJS unsubscription
(e.g. navigating away from the workspace while thumbnail loads are
pending), the AbortController.abort() call triggers a catch handler
that previously relied solely on a @unsubscribed? flag to suppress the
error.
This was unreliable: nested observables spawned inside rx/mapcat (such
as datauri->blob-uri conversions within get-file-object-thumbnails)
could abort independently, with their own AbortController instances,
meaning the outer unsubscribed? flag was never set and the AbortError
propagated as an unhandled exception.
Add an explicit AbortError name check as a disjunctive condition so
that abort errors originating from any observable in the chain are
suppressed at the source, regardless of subscription state.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix dissoc error when detaching stroke color from library
The detach-value function in color-row was only passing index to
on-detach, but the stroke's on-color-detach handler expects both
index and color arguments. This caused a protocol error when trying
to dissoc from a number instead of a map.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix crash when detaching color asset from stroke
The color_row detach-value callback calls on-detach with (index, color),
but stroke_row's local on-color-detach wrapper only took a single argument
(fn [color] ...), so it received index as color and passed it to
stroke.cljs which then called (dissoc index :ref-id :ref-file), crashing
with 'No protocol method IMap.-dissoc defined for type number'.
Fix the wrapper to accept (fn [_ color] ...) so it correctly ignores the
index passed by color_row (it already has index in the closure) and
forwards the actual color map to the parent handler.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>