{"record":{"id":"75953bb3ecdb4192","repo":"instructure/canvas-lms","slug":"submission-not-found-in-connection-items","errorCode":null,"errorMessage":"submission not found in connection items","messagePattern":"submission not found in connection items","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"app/graphql/patched_array_connection.rb","lineNumber":31,"sourceCode":"# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR\n# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more\n# details.\n#\n# You should have received a copy of the GNU Affero General Public License along\n# with this program. If not, see <http://www.gnu.org/licenses/>.\n#\n\nclass PatchedArrayConnection < GraphQL::Pagination::ArrayConnection\n  # The default ArrayConnection uses `find_index(item)` which uses `==` to get\n  # the index. Unfortunately submission histories are saved through versionable\n  # which returns an array of submission histories that all share the same id,\n  # and active record overrides the `==` method to check for equality based on\n  # said id. When dealing with submissions, change the comparator to look at the\n  # submitted_at time (what submission#submission_history is doing) instead of\n  # by id so cursors don't break.\n  def cursor_for_submission_node(submission)\n    submission_idx = items.find_index { |i| i.submitted_at.to_i == submission.submitted_at.to_i }\n    raise ArgumentError, \"submission not found in connection items\" unless submission_idx\n\n    encode((submission_idx + 1).to_s)\n  end\n\n  def cursor_for_quiz_submission_node(quiz_submission)\n    # Use object identity (not AR `==` which compares by id) since all items\n    # in the array share the same quiz_submission id but represent different\n    # attempts via simply_versioned deserialization.\n    quiz_submission_idx = items.find_index { |i| i.equal?(quiz_submission) }\n    raise ArgumentError, \"quiz_submission not found in connection items\" unless quiz_submission_idx\n\n    encode((quiz_submission_idx + 1).to_s)\n  end\n\n  def cursor_for(item)\n    return cursor_for_submission_node(item) if item.instance_of?(Submission)\n    return cursor_for_quiz_submission_node(item) if item.instance_of?(Quizzes::QuizSubmission)\n","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/patched_array_connection.rb#L13-L49","documentation":"PatchedArrayConnection overrides cursor computation for Submission nodes, matching items by submitted_at timestamp instead of AR id equality (submission history shares ids). If the given submission's submitted_at does not match any item in the connection's items array, it raises this ArgumentError — the node is not part of the page used to build cursors.","triggerScenarios":"Requesting a cursor (via cursor_for / pageInfo on a node) for a Submission whose submitted_at.to_i differs from every item in items — the submission belongs to another page of results, was resubmitted so its submitted_at changed after items were loaded, or the node was fabricated client-side.","commonSituations":"Paginated submission-history queries where a resubmission updates submitted_at between loading items and requesting the cursor; asking for a cursor of a submission outside the current page; custom loaders passing nodes that did not originate from this connection's items array.","solutions":["Only request cursors for nodes that came from this connection's items in the same request/page","Re-fetch the connection so items and node are consistent (resubmission changes submitted_at)","Verify the submission is part of the current page before computing the cursor","If comparing history versions, ensure the exact serialized version (same submitted_at) is in items"],"exampleFix":"// before\nconst cursor = submissionConnection.cursorFor(someSubmission) // node from another page\n// after\nconst item = submissionConnection.items.find(i => i.id === someSubmission.id)\nconst cursor = item ? submissionConnection.cursorFor(item) : null","handlingStrategy":"validation","validationCode":"const inPage = connection.items.some(i => Math.floor(Date.parse(i.submittedAt) / 1000) === Math.floor(Date.parse(node.submittedAt) / 1000))\nif (!inPage) throw new Error('submission not on this page; cannot compute cursor')","typeGuard":null,"tryCatchPattern":"try {\n  const cursor = connection.cursorFor(node)\n} catch (e) {\n  if (/submission not found in connection items/.test(e.message)) {\n    await refetchConnection() // node/page drifted (e.g. resubmission)\n  } else throw e\n}","preventionTips":["Only compute cursors for nodes taken from the same page of items","Refetch the connection after submissions are resubmitted (submitted_at changes)","Never fabricate nodes client-side and ask the connection for their cursor"],"tags":["graphql","pagination","cursor","argument-error"],"backgroundTag":"resource-not-found","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}