{"record":{"id":"a5d9153229f3f2b4","repo":"laurent22/joplin","slug":"cannot-send-empty-content","errorCode":null,"errorMessage":"Cannot send empty content","messagePattern":"Cannot send empty content","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/app-clipper/popup/src/bridge.js","lineNumber":389,"sourceCode":"\t\t}\n\n\t\tconst response = await fetch(`${baseUrl}/${path}${queryString}`, fetchOptions);\n\t\tif (!response.ok) {\n\t\t\tconst msg = await response.text();\n\t\t\tthrow new Error(msg);\n\t\t}\n\n\t\tconst json = await response.json();\n\t\treturn json;\n\t}\n\n\tasync sendContentToJoplin(content) {\n\t\tconsole.info('Popup: Sending to Joplin...');\n\n\t\ttry {\n\t\t\tthis.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: true } });\n\n\t\t\tif (!content) throw new Error('Cannot send empty content');\n\n\t\t\t// There is a bug in Chrome that somehow makes the app send the same request twice, which\n\t\t\t// results in Joplin having the same note twice. There's a 2-3 sec delay between\n\t\t\t// each request. The bug only happens the first time the extension popup is open and the\n\t\t\t// Complete button is clicked.\n\t\t\t//\n\t\t\t// It's beyond my understanding how it's happening. I don't know how this sendContentToJoplin function\n\t\t\t// can be called twice. But even if it is, logically, it's impossible that this\n\t\t\t// call below would be done with twice the same nounce. Even if the function sendContentToJoplin\n\t\t\t// is called twice in parallel, the increment is atomic and should result in two nounces\n\t\t\t// being generated. But it's not. Somehow the function below is called twice with the exact same nounce.\n\t\t\t//\n\t\t\t// It's also not something internal to Chrome that repeat the request since the error is caught\n\t\t\t// so it really seems like a double function call.\n\t\t\t//\n\t\t\t// So this is why below, when we get the duplicate nounce error, we just ignore it so as not to display\n\t\t\t// a useless error message. The whole nounce feature is not for security (it's not to prevent replay\n\t\t\t// attacks), but simply to detect these double-requests and ignore them on Joplin side.","sourceCodeStart":371,"sourceCodeEnd":407,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/app-clipper/popup/src/bridge.js#L371-L407","documentation":"Thrown by the web-clipper popup's sendContentToJoplin when the content argument is falsy (empty string, null, undefined). The bridge refuses to POST an empty body to the Joplin API because it would create an empty or malformed note.","triggerScenarios":"Calling sendContentToJoplin(content) with an empty string or null/undefined. This can happen when the clipper captures no selected text, the page body extraction returns nothing, or the user clicks the send button before content is populated.","commonSituations":"Clipping a page that has no extractable body (e.g. a pure-canvas app); the content extraction ran before the DOM was ready; a browser extension race where the popup opens before the content script returns data; double-invocation bug noted in the surrounding comments.","solutions":["Verify the captured content is non-empty before calling sendContentToJoplin (guard at the call site).","Inspect the page-extraction step (the code that builds `content`) for empty returns on the target page.","Disable and re-enable the clipper extension to reset popup state if a stale empty payload is being sent.","Update the clipper extension to the latest version — the surrounding code references a known double-send Chrome bug."],"exampleFix":"// before\nif (!content) throw new Error('Cannot send empty content');\n\n// after — guard the caller and explain why\nif (!content || !content.trim()) {\n  throw new Error('Cannot send empty content — no text was captured from the page.');\n}","handlingStrategy":"validation","validationCode":"if (!content || !content.trim()) {\n  console.warn('No content captured; not sending.');\n  return;\n}\nawait bridge.sendContentToJoplin(content);","typeGuard":"function hasSendableContent(content: unknown): content is string {\n  return typeof content === 'string' && content.trim().length > 0;\n}","tryCatchPattern":"try {\n  await bridge.sendContentToJoplin(content);\n} catch (e) {\n  if (e.message === 'Cannot send empty content') {\n    // show the user that nothing was captured; do not retry\n  } else throw e;\n}","preventionTips":["Guard sendContentToJoplin callers with a non-empty check including .trim().","Verify page content extraction returned a value before enabling the send button.","Re-load the clipper extension if the popup holds stale empty state."],"tags":["clipper","browser-extension","validation","user-input"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}