{"record":{"id":"5c74984a6c761165","repo":"laurent22/joplin","slug":"unhandled-resource-resourceid","errorCode":null,"errorMessage":"Unhandled resource: ${resourceId}","messagePattern":"Unhandled resource: (.+?)","errorType":"http","errorClass":"Error","httpStatus":400,"severity":"warning","filePath":"packages/app-cli/app/ResourceServer.ts","lineNumber":72,"sourceCode":"\t\tthis.server_.on('request', async (request, response) => {\n\t\t\tconst writeResponse = (message: string) => {\n\t\t\t\tresponse.write(message);\n\t\t\t\tresponse.end();\n\t\t\t};\n\n\t\t\tconst url = urlParser.parse(request.url, true);\n\t\t\tconst pathParts = url.pathname.split('/');\n\t\t\tif (pathParts.length < 2) {\n\t\t\t\twriteResponse(`Error: could not get resource ID from path name: ${url.pathname}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst resourceId = pathParts[1];\n\n\t\t\tif (!this.linkHandler_) throw new Error('No link handler is defined');\n\n\t\t\ttry {\n\t\t\t\tconst done = await this.linkHandler_(resourceId, response);\n\t\t\t\tif (!done) throw new Error(`Unhandled resource: ${resourceId}`);\n\t\t\t} catch (error) {\n\t\t\t\tresponse.setHeader('Content-Type', 'text/plain');\n\t\t\t\t// eslint-disable-next-line require-atomic-updates\n\t\t\t\tresponse.statusCode = 400;\n\t\t\t\tresponse.write(error.message);\n\t\t\t}\n\n\t\t\tresponse.end();\n\t\t});\n\n\t\tthis.server_.on('error', error => {\n\t\t\tthis.logger().error('Resource server:', error);\n\t\t});\n\n\t\tthis.server_.listen(this.port_);\n\n\t\tenableServerDestroy(this.server_);\n","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/app-cli/app/ResourceServer.ts#L54-L90","documentation":"ResourceServer is a loopback HTTP server (ports 9167/9267/8167/8267) that serves embedded resources and internal links when note markdown is rendered in the terminal GUI. For each request it extracts the resource ID from the URL path and delegates to a registered link handler (LinkHandler). If the handler returns false — meaning it could not process that resource ID — the server throws 'Unhandled resource'. The throw is immediately caught by the surrounding try/catch and returned to the HTTP client as a 400 response whose body is the error message, so it does not crash the process.","triggerScenarios":"The link handler registered in app-gui.ts looks up noteLinks[resourceId] and only handles link.type === 'url' and link.type === 'item', returning false otherwise. 'Unhandled resource' fires when a link object exists but its type is neither 'url' nor 'item', or when the handler falls through without writing a response and returns false.","commonSituations":"A note contains a link whose type the handler does not cover (e.g. a new/future link type); noteLinks is stale after the note was edited but not re-parsed; a plugin or custom markdown renderer emits a link type the built-in handler does not recognize.","solutions":["Inspect the 400 response body — it contains the offending resourceId; cross-reference it with the noteLinks map to find its link.type.","Extend the link handler in app-gui.ts with a branch for the missing link type, or write a fallback response and return true so the handler never returns false.","Re-render/re-parse the note to rebuild noteLinks if you suspect staleness after editing.","Check the note markdown for malformed or non-standard links."],"exampleFix":"// before (app-gui.ts link handler returns false for unknown types)\n//   if (link.type === 'item') { ...; return true; }\n//   return false; // -> 'Unhandled resource: <id>'\n// after\n//   if (link.type === 'item') { ...; return true; }\n//   response.statusCode = 404;\n//   response.write(`Unsupported link type: ${link?.type ?? 'unknown'}`);\n//   return true;","handlingStrategy":"validation","validationCode":"const link = noteLinks[resourceId];\nif (!link || !['url', 'item'].includes(link.type)) {\n  response.statusCode = 404;\n  response.end(`Unknown link type: ${link?.type ?? 'none'}`);\n  return true; // mark handled so ResourceServer does not throw\n}","typeGuard":"const isKnownLink = (link: unknown): link is { type: 'url' | 'item' } =>\n  !!link && typeof link === 'object' &&\n  (link.type === 'url' || link.type === 'item');","tryCatchPattern":null,"preventionTips":["Make the link handler cover every link type your notes can produce, or return true after writing a fallback response so it never returns false.","Rebuild noteLinks whenever the note content changes to avoid stale lookups.","Log the resourceId and link.type for any 400 response to catch new link types early."],"tags":["resource-server","http","link-handler","terminal-gui","loopback"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}