Skip to content

Boards with a whitespace-only name fail to sync and retry forever #2281

Description

@RodriSanchez1

Summary

A board whose name is whitespace-only (e.g. " ") is accepted by the client and by Swagger validation, but rejected by Mongoose, so the board can never sync. Because the sync engine never rolls back the PENDING status on failure, the board is re-pushed on every sync cycle indefinitely.

Symptoms

Two API error signatures, one root cause:

Handler Code Message
cboard-api api/controllers/board.js:37 createBoard 409 Error saving board
cboard-api api/controllers/board.js:263 updateBoard 500 Error saving board. (trailing period + space)

Client-side these surface as repeated Sync exceptions from phase: 'pushBoard'.

Root cause

Every gate in the chain lets a whitespace-only name through, and the last one rejects it:

  1. src/components/Board/TileEditor/TileEditor.component.js:532disableSubmit={!currentLabel}. '' is falsy so an empty label is correctly blocked, but ' ' is truthy, so Save is enabled.
  2. src/components/Board/TileEditor/TileEditor.component.js:331-332handleLabelChange clears labelKey, so the new board gets name: ' ', nameKey: ''.
  3. src/components/Board/Board.container.js:588 / :1172 — folder board is created with name: tile.label.
  4. src/components/Board/Board.utils.js:62extractBoardName returns board.name unchanged because ' ' is truthy.
  5. cboard-api api/swagger/swagger.yaml:1522-1532name is required with type: string and no minLength, so ' ' passes. (required here means the key must be present, not non-empty.)
  6. cboard-api api/models/Board.js:8-12name: { type: String, required: true, trim: true }. The trim setter runs on assignment, before the required validator, so ' ' becomes '' and validation fails.

The PUT handler merges rather than overwrites (for (let key in updateData) board[key] = updateData[key]), so a payload that omits name is harmless. The failure specifically requires name present and blank-after-trim — which is why the stored DB row still holds the original name: every write was rejected, so the server value was never touched.

author is vulnerable in exactly the same way. transformBoardForUser (src/components/Board/Board.utils.js:83) sets author: userName || userEmail; a display name of ' ' is truthy, survives the ||, and dies at the same trim + required pair. name, author and email are the only three required + trim fields on the model.

Why it never self-recovers

src/components/Board/Board.reducer.js:236-247UPDATE_BOARD replaces the board wholesale and sets SYNC_STATUS.PENDING. The blank name is persisted locally before the API call and is never rolled back on failure; the catch at src/components/Board/Board.actions.js:888 only logs and calls trackSyncException. The board stays PENDING and is re-pushed every cycle forever — one bad board produces unbounded telemetry.

Telemetry gap

The API already returns the precise Mongoose message (error: err.message at board.js:38 and board.js:262), which names the failing field. But src/api/api.js returns the raw axios rejection, whose .message is only "Request failed with status code 500", and trackSyncException (src/components/Board/Board.sync.analytics.js:30-39) records only exception plus { phase, boardId }. The server's field-level diagnosis was being discarded at the client boundary, which is why the failing field had to be inferred rather than read.

Notes on scope

  • Empty labels are not the cause. disableSubmit correctly blocks '', so a folder board can never be created with an empty name. Only whitespace gets through.
  • Commit 78866f6 is not the regression. That commit changed extractBoardName's fallback from 'Untitled Board' to '', but that branch requires a board with neither name nor nameKey. All 44 default boards in src/api/boards.json carry a nameKey that derives a non-blank name (verified: zero with neither, zero whose nameKey last segment is blank), and the tile path always supplies a truthy name. The '' branch appears unreachable — it is a latent hazard, not the live bug. The absence of logs before the sync-engine deploy is explained by 7a4c2d47 introducing the retry loop, not by 78866f65.
  • nameKey exists in neither the Swagger schema nor the Mongoose model, so it is dropped on every round trip. Combined with the wholesale UPDATE_BOARD replace, any board that has been pulled once has no local nameKey, making the extractBoardName fallback dead for those boards.

Fixes

  • Capture e.response.data.error and status in trackSyncException for phase: 'pushBoard', so production reports the failing field instead of requiring inference.
  • disableSubmit={!currentLabel || !currentLabel.trim()} in TileEditor.
  • Deferred: trim name, author and email at the wire in API.createBoard / API.updateBoard. This remains the only fix that covers boards which never pass through transformBoardForUser — a user-owned board goes straight to updateApiBoard and bypasses it, as do boards already persisted in localStorage with a bad name. Without it, existing affected boards are not repaired.
  • Restore a DEFAULT_BOARD_NAME fallback in extractBoardName, and trim userName before the || in transformBoardForUser.
  • Not addressed: the unbounded retry loop. Any permanently-rejected board is still re-pushed every cycle. Needs an attempt counter in syncMeta that skips a board past a threshold and resets on local edit or success — a change to the sync state machine that interacts with the graduation logic in classifyBoardsForPush, so it warrants its own issue.

Open question

The originally-reported board reads people in the DB, which derives from nameKey: cboard.symbol.people and is non-blank under every code path examined. No path was found that blanks that board's name. Either the failing board is not the one being inspected, or it is failing on author rather than name. The telemetry fix above should resolve this on the next sync cycle from an affected user.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions