{"record":{"id":"2a29ad5ece543441","repo":"outline/outline","slug":"msg-2a29ad","errorCode":null,"errorMessage":"msg","messagePattern":"msg","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/models/validators/TextLength.ts","lineNumber":34,"sourceCode":"  msg?: string;\n  min?: number;\n  max: number;\n}): (target: object, propertyName: string) => void {\n  return (target: object, propertyName: string) =>\n    addAttributeOptions(target, propertyName, {\n      validate: {\n        validLength(value: ProsemirrorData) {\n          let text;\n\n          try {\n            text = ProsemirrorHelper.toPlainText(Node.fromJSON(schema, value));\n          } catch (_err) {\n            throw new Error(\"Invalid data\");\n          }\n\n          const length = text ? Array.from(text).length : 0;\n          if (length > max || length < min) {\n            throw new Error(msg);\n          }\n        },\n      },\n    });\n}\n","sourceCodeStart":16,"sourceCodeEnd":40,"githubUrl":"https://github.com/outline/outline/blob/935a44d4c003429645a956141b2c4eb695f34b6e/server/models/validators/TextLength.ts#L16-L40","documentation":"This is a Sequelize custom validator (`TextLength`) attached to the `Comment.data` JSONB column in server/models/Comment.ts:53. It renders the stored ProseMirror document to plain text via `ProsemirrorHelper.toPlainText`, counts Unicode code points with `Array.from(text).length` (so emoji and CJK characters count as one each, not by UTF-16 code unit), and throws the supplied `msg` when the count falls outside `[min, max]`. The active ceiling is `CommentValidation.maxLength = 1000` characters, so the thrown message reads \"Comment must be less than 1000 characters\". It fires during model validation on any create or update that persists the `data` field.","triggerScenarios":"Creating or updating a Comment (`POST /api/comments.create`, `/api/comments.update`) whose rendered plain text exceeds 1000 Unicode code points. Because the count is on extracted text, a document containing many short nodes (list items, mentions) can hit the limit even when the raw JSON feels small. It also fires when `min > 0` is configured and the text is too short, though the Comment usage leaves `min` at its default of 0.","commonSituations":"Pasting a large block of text or a long threaded reply into a comment; programmatic imports/bot integrations that submit untruncated bodies; migrations or test fixtures that seed oversized comments; emoji-heavy content where byte-length estimates undercount because the validator measures code points, not bytes.","solutions":["Render the candidate ProseMirror doc to plain text and measure `Array.from(text).length` client-side before submitting; truncate or block the post when it exceeds 1000.","Split the content across multiple comments or move long content into a Document (DocumentValidation.maxLength is far higher).","If you control the editor, enforce the limit with the MaxLength ProseMirror extension (shared/editor/extensions/MaxLength.ts) so the UI prevents the over-length state.","Verify the `data` blob is valid ProseMirror JSON — a malformed node throws the separate \"Invalid data\" error at TextLength.ts:29 before the length check."],"exampleFix":"// before — submit unvalidated comment data\nawait client.comments.create({ documentId, data });\n\n// after — measure rendered text length first\nimport { ProsemirrorHelper } from \"@shared/utils/ProsemirrorHelper\";\nimport { Node } from \"prosemirror-model\";\nimport { schema } from \"@server/editor\";\nimport { CommentValidation } from \"@shared/validations\";\n\nconst text = ProsemirrorHelper.toPlainText(Node.fromJSON(schema, data));\nif (Array.from(text).length > CommentValidation.maxLength) {\n  throw new Error(`Comment must be less than ${CommentValidation.maxLength} characters`);\n}\nawait client.comments.create({ documentId, data });","handlingStrategy":"validation","validationCode":"import { CommentValidation } from \"@shared/validations\";\nimport { ProsemirrorHelper } from \"@shared/utils/ProsemirrorHelper\";\nimport { Node } from \"prosemirror-model\";\nimport { schema } from \"@server/editor\";\n\n/**\n * Returns true when the rendered text length fits the Comment limit.\n *\n * @param data ProseMirror document data.\n * @return true if the comment is safe to persist.\n */\nfunction isCommentLengthValid(data: ProsemirrorData): boolean {\n  const text = ProsemirrorHelper.toPlainText(Node.fromJSON(schema, data));\n  return Array.from(text).length <= CommentValidation.maxLength;\n}","typeGuard":"/**\n * Narrows a value to a valid-length Comment data object.\n *\n * @param value Candidate ProseMirror document data.\n * @return true when value parses and is within the length limit.\n */\nfunction isValidCommentData(value: unknown): value is ProsemirrorData {\n  try {\n    const node = Node.fromJSON(schema, value as ProsemirrorData);\n    const text = ProsemirrorHelper.toPlainText(node);\n    return Array.from(text).length <= CommentValidation.maxLength;\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":"try {\n  await comment.save();\n} catch (err) {\n  if (err instanceof Sequelize.ValidationError\n      && err.errors.some((e) => e.message.startsWith(\"Comment must be less than\"))) {\n    // truncate or prompt the user to shorten the comment\n  } else {\n    throw err;\n  }\n}","preventionTips":["Measure rendered text length with Array.from(text).length, not string .length, to match the validator's Unicode counting.","Wire the MaxLength ProseMirror extension into the comment editor so over-length input is blocked at the UI.","Run client-side validation with the same CommentValidation.maxLength constant shared between client and server.","Treat 'Invalid data' (a separate throw at TextLength.ts:29) as a malformed-doc bug, not a length problem."],"tags":["validation","prosemirror","sequelize","comment","unicode"],"backgroundTag":null,"analyzedSha":"935a44d4c003429645a956141b2c4eb695f34b6e","analyzedAt":"2026-08-12T22:40:11.882Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}