{"record":{"id":"c8a2f4fda0aae668","repo":"antiwork/gumroad","slug":"something-went-wrong-c8a2f4","errorCode":null,"errorMessage":"Something went wrong.","messagePattern":"Something went wrong\\.","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/components/Product/Layout.tsx","lineNumber":79,"sourceCode":"      header: \"\",\n      hide_header: true,\n    });\n    return sections;\n  });\n\n  const saveSections = async (sections: EditableSection[]) => {\n    setSections(sections);\n    const order = sections.map((section) => section.id);\n    const mainIndex = order.findIndex((id) => !id);\n    order.splice(mainIndex, 1);\n    try {\n      const response = await request({\n        method: \"PUT\",\n        url: Routes.sections_link_path(product.permalink),\n        accept: \"json\",\n        data: { sections: order, main_section_index: mainIndex },\n      });\n      if (!response.ok) throw new ResponseError();\n      showAlert(\"Changes saved!\", \"success\");\n    } catch (e) {\n      assertResponseError(e);\n      showAlert(e.message, \"error\");\n    }\n  };\n\n  const sectionsRef = useRefToLatest(sections);\n  const dispatch = (action: Action) => {\n    const sections = sectionsRef.current;\n    switch (action.type) {\n      case \"add-section\": {\n        action.section.then((section) => {\n          const newSections = [...sections];\n          newSections.splice(action.index, 0, section);\n          void saveSections(newSections);\n        }, assertResponseError);\n        break;","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/components/Product/Layout.tsx#L61-L97","documentation":"saveSections persists a reordered section list via PUT sections_link_path(permalink) with the ordered ids and main_section index. A non-ok response throws a bare ResponseError, caught and shown as 'Something went wrong.'. Crucially, setSections(sections) already ran optimistically and there is no rollback in the catch — the UI keeps the failed order until reload, silently diverging from the server. Also note the latent trap: when findIndex returns -1 (no unsaved/main section), splice(-1, 1) removes the LAST section instead of none.","triggerScenarios":"PUT returning 404 (permalink renamed in another tab — the URL is built from a stale permalink), 401 (expired session), or 422 (an id in the order array the server does not accept, or main_section_index out of range).","commonSituations":"Two tabs editing the same product — one renames the permalink, the other's next drag-and-drop save 404s; a section deleted server-side between load and reorder; seller reorders while logged out in another tab.","solutions":["Check the Network tab for the PUT sections_link status: 404 means stale permalink — reload the editor; 422 means an invalid section order/id.","Reload the page after a failure so the UI order re-syncs with the server (the current code leaves them diverged).","Guard the -1 findIndex case before splicing (see defense).","On 422, re-fetch the section list to reconcile ids that may have changed server-side.","Surface a reload prompt in the alert so sellers know their drag was not saved."],"exampleFix":"// before\nconst mainIndex = order.findIndex((id) => !id);\norder.splice(mainIndex, 1);\n\n// after — findIndex returning -1 made splice(-1, 1) silently drop the LAST section\nconst mainIndex = order.findIndex((id) => !id);\nif (mainIndex === -1) throw new ResponseError('No main section found — reload the page.');\norder.splice(mainIndex, 1);","handlingStrategy":"validation","validationCode":"const order = sections.map((section) => section.id);\nconst mainIndex = order.findIndex((id) => !id);\nif (mainIndex === -1) {\n  showAlert('No main section found — reload the page and try again.', 'error');\n  return; // guards the splice(-1, 1) trap that silently drops the last section\n}","typeGuard":"const areSectionIds = (ids: unknown[]): ids is string[] => ids.every((id) => typeof id === 'string');","tryCatchPattern":"try {\n  const response = await request({ method: 'PUT', url: Routes.sections_link_path(product.permalink), accept: 'json', data: { sections: order, main_section_index: mainIndex } });\n  if (!response.ok) throw new ResponseError();\n} catch (e) {\n  assertResponseError(e);\n  setSections(sectionsRef.current); // roll back the optimistic reorder so UI matches server\n  showAlert('Changes could not be saved. Reload the page and try again.', 'error');\n}","preventionTips":["Guard mainIndex === -1 before splice — splice(-1, 1) removes the LAST section, corrupting the order you send.","Roll back the optimistic setSections on failure; without it the UI keeps an unsaved order until reload.","Build the URL from a permalink fetched at save time when multiple tabs may edit the product.","On 404, prompt a reload — the permalink changed under you and every subsequent save will fail too."],"tags":["http","product-sections","optimistic-ui","ordering","response-not-ok"],"backgroundTag":"http-request-failed","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}