Skip to content

Settings: "Save profile" has never worked — it sends theme: null and gets HTTP 500 #171

Closed

Description

@Adron

The Save Profile button returns 500 every time

Found while reviewing #46's work (fixed there — filed for visibility, since a shipped-broken button deserves a tracked identity).

SettingsViewModel calls:

await _session.Api.UpdateProfileAsync(DisplayName, Bio, IsPrivateAccount);

UpdateProfileAsync has a 4th parameter defaulting to null:

public Task UpdateProfileAsync(string? displayName, string? bio,
                               bool? isPrivateAccount = null, string? theme = null, …)
    => SendVoidAsync(HttpMethod.Patch, "api/user/update",
        new { displayName, bio, isPrivateAccount, theme }, ct);

JsonSerializerDefaults.Web does not drop nulls, so the body always carries "theme": null — and the server rejects an explicit null.

Reproduced live (2026-09-16)

A) exactly what Save profile sends today:
   {"displayName":"…","bio":"…","isPrivateAccount":false,"theme":null}
   -> HTTP 500  {"error":"Internal server error","code":"internal_error"}

B) identical body with the null key removed:
   {"displayName":"…","bio":"…","isPrivateAccount":false}
   -> HTTP 200  {"message":"User updated successfully","user":{…}}

So editing your display name, bio or private-account flag from this app has never worked. The catch (InterlinedApiException) shows the message, so it isn't silent — it just always fails.

Root cause worth generalising

Any writer that builds a PATCH body from an anonymous object with optional parameters will ship nulls for the ones the caller omitted. On this API an explicit null is a 500, not a no-op, so every such writer is a latent 500. PATCH bodies must be built sparsely — only the fields actually being changed.

Fix

Already implemented in #167 (issue-46-honor-preferences): both profile writers build a sparse dictionary instead of an anonymous object.

Acceptance criteria

Related live findings from the same work

  • The OpenAPI request schema for /api/user/update types every field as "string". That's wrong — native JSON numbers and booleans are correct (the response schema uses native types).
  • viewingPreference is enum-validated; the server names the values in its 400: my_messages, all_messages, followers_only, following_only.
  • theme is not validated — it accepted and stored "zzz_bogus".
  • Live ranges from 400s: maxMessageLength 1–10000, messagesPerPage 10–30, notificationTrayLimit 10–40.
  • The PATCH 200's user is narrower than GET /api/user (no accountStatus/cleared/pendingEmail/isAdministrator) — deserializing it into CurrentUser would blank those fields, so callers must re-fetch.

Activity

  1. added
    bugSomething isn't working
    P0Must have for a credible parity claim
    on Sep 16, 2026
  2. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    Fixed in #167 (issue-46-honor-preferences), which builds both profile writers as sparse dictionaries.

    Filed for visibility rather than as new work — a button that has never worked deserves a tracked identity, and the root cause generalises: any PATCH body built from an anonymous object with optional parameters ships nulls for the omitted ones, and on this API an explicit null is a 500, not a no-op. The second acceptance criterion (auditing other writers for the same defect) is genuinely open.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Must have for a credible parity claimarea:settingsArea: settingsbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions