Skip to content

Render server validation errors as form errors - #421

Open
neo22neo wants to merge 2 commits into
interstellar-app:mainfrom
neo22neo:fix/community-name-400-inline-error
Open

Render server validation errors as form errors#421
neo22neo wants to merge 2 commits into
interstellar-app:mainfrom
neo22neo:fix/community-name-400-inline-error

Conversation

@neo22neo

@neo22neo neo22neo commented Sep 5, 2026

Copy link
Copy Markdown

Stacked on #420. This branch is based on fix/mbin-community-name-validation,
so until #420 merges this PR shows that commit and its files too. The only work
owned by this PR is the last commit, Render server validation errors as form errors (lib/src/api/client.dart + a small delta in
lib/src/screens/explore/community_owner_panel.dart). Once #420 is merged I'll
rebase so the diff here collapses to just that. Please merge #420 first.

Problem

#420 stops predictable bad names — anything outside Mbin's
^[a-zA-Z0-9_]{2,25}$ — before the request is ever sent. But the server still
rejects names for reasons a client cannot know in advance: already taken,
reserved, or blocked by instance policy. Those come back as 400 with an
RFC 7807-style body:

{
  "type": "https://tools.ietf.org/html/rfc2616#section-10",
  "title": "An error occurred",
  "status": 400,
  "detail": "..."
}

ServerClient.checkResponseSuccess embedded that entire JSON body into an
http.ClientException message, and the create screen had no try/catch, so
it reached the global handler in main.dart and rendered as a snackbar full of
raw JSON. The useful detail was buried inside it.

So: #420 handles malformed names before submit; this PR makes the server's own
validation responses render as form errors instead of raw
ClientException/JSON
. It's the backstop, not the primary fix.

Changes

lib/src/api/client.dart

  • New ServerErrorException carrying statusCode / uri / title / detail /
    rawBody, with toString() returning detail ?? title ?? <generic>.
  • checkResponseSuccess now tries to parse an error body as a JSON object with
    a string title/detail and throws ServerErrorException when it can.
    Bodies that aren't structured problem JSON — Lemmy/PieFed {"error": ...},
    HTML, empty — still throw http.ClientException exactly as before. No call
    site in the repo matches ClientException by type, so this is backwards
    compatible and incidentally means every generic catch (e) now shows a clean
    message instead of a JSON blob.

lib/src/screens/explore/community_owner_panel.dart

  • create() is wrapped in try / on ServerErrorException: a 400 carrying a
    detail is shown on the Name field through the errorText plumbing added in
    Validate Mbin community names when creating a community #420, and swallowed so no snackbar fires. Anything else is rethrown to the
    existing generic handling.
  • The message is stored alongside the name it was returned for and only shown
    while the field still holds that name, so it clears itself on any edit —
    including Validate Mbin community names when creating a community #420's suggestion button, which sets the controller text directly
    and so never fires onChanged.
  • The create/edit ternary became an if/else so only the create call is
    wrapped; the edit path is behaviourally unchanged.

This PR deliberately contains no validation, helper-text, suggestion, or
TextEditor changes — all of that is #420's.

Testing

No Flutter toolchain in this environment, so dart format / flutter test /
the app itself were not run locally — CI is the first real check. Verified by
hand: the sample body above parses with detail as a String, and no line in
the changed files exceeds 80 columns.

Manual test plan (Mbin account), after #420:

  1. Name that Validate Mbin community names when creating a community #420 already rejects → inline error, submit blocked, no request.
  2. Well-formed name that the server rejects anyway (e.g. one already taken) →
    the server's detail appears under the Name field, no snackbar.
  3. Edit the name → the server error clears; tap Validate Mbin community names when creating a community #420's suggestion button → it
    also clears.
  4. Force an unrelated failure (offline, 500) → still the generic snackbar.
  5. Lemmy/PieFed creation and the Mbin community edit screen unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_015icyWnYYmR7J8oSHocta6U

Mbin rejects magazine names that fall outside /^[a-zA-Z0-9_]{2,25}$/, but
the create-community form only disabled submit on an empty name, so an
invalid name failed server-side with no guidance.

- Add mbin_community_name.dart with pure, tested helpers to validate a
  name, describe why it is invalid, and derive a sanitized suggestion.
- Give the shared TextEditor helperText/errorText support.
- In CommunityOwnerPanelGeneral (creation only), on Mbin: show the rule as
  helper text, an inline error while the name is invalid, a one-tap
  "Use suggestion" action, and keep submit disabled until the name is
  valid. Lemmy/PieFed behaviour is unchanged.
- Add flutter_test dev dependency and focused unit tests for the helpers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7b7QotawaBJQ4isQKd14x
@neo22neo

neo22neo commented Sep 5, 2026

Copy link
Copy Markdown
Author

@jwr1 could you take a look when you have a chance? This turns the Mbin 400 name-validation response into an inline field error instead of the raw-JSON snackbar. I couldn't run dart format locally (no Flutter toolchain here), so if the analysis job flags whitespace I'll push a formatting-only fixup.

Mbin rejects some community names for reasons the client cannot predict
(already taken, reserved, instance policy) with a 400 whose RFC 7807-style
body carries a human-readable `detail`. `checkResponseSuccess` embedded that
whole JSON body in an `http.ClientException` message, and the create screen
had no try/catch, so it surfaced through the global handler as a snackbar
full of raw JSON.

- Add `ServerErrorException`, thrown by `checkResponseSuccess` when an error
  body is a JSON object with a string `title`/`detail`. Its `toString()`
  returns `detail`/`title`, so every existing generic `catch (e)` renders a
  clean message instead of raw JSON. Non-structured bodies (Lemmy/PieFed
  `{"error": ...}`, HTML, empty) still throw `ClientException` unchanged, and
  no call site matches that type.
- Community create screen: catch `ServerErrorException` from `create()`; a 400
  carrying a `detail` is shown on the Name field through the existing
  `errorText` plumbing and swallowed, so no snackbar fires. Anything else is
  rethrown to the existing generic handling.
- The message is tracked alongside the name it was returned for, so it clears
  itself on any edit, including the suggestion button setting the controller
  text directly.

This is a backstop behind the client-side validation, which already blocks
predictable malformed names before submit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015icyWnYYmR7J8oSHocta6U
@neo22neo
neo22neo force-pushed the fix/community-name-400-inline-error branch from 5e79981 to 8c71089 Compare September 5, 2026 18:48
@neo22neo neo22neo changed the title Show Mbin community-name validation errors inline Render server validation errors as form errors Sep 5, 2026
@neo22neo

neo22neo commented Sep 5, 2026

Copy link
Copy Markdown
Author

Restructured this PR: it was independently duplicating #420's client-side validation and TextEditor changes. It's now stacked on fix/mbin-community-name-validation and reduced to only the server-error handling — ServerErrorException in api/client.dart plus a small delta in the create screen that routes a 400's detail through the errorText plumbing #420 adds. All duplicated validation/helper/TextEditor code has been removed. Please merge #420 first; I'll rebase afterwards so the diff here collapses to just the last commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant