laurent22/joplin · warning · Error

Unhandled resource: ${resourceId}

Error message

Unhandled resource: ${resourceId}

What it means

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.

Source

Thrown at packages/app-cli/app/ResourceServer.ts:72

		this.server_.on('request', async (request, response) => {
			const writeResponse = (message: string) => {
				response.write(message);
				response.end();
			};

			const url = urlParser.parse(request.url, true);
			const pathParts = url.pathname.split('/');
			if (pathParts.length < 2) {
				writeResponse(`Error: could not get resource ID from path name: ${url.pathname}`);
				return;
			}
			const resourceId = pathParts[1];

			if (!this.linkHandler_) throw new Error('No link handler is defined');

			try {
				const done = await this.linkHandler_(resourceId, response);
				if (!done) throw new Error(`Unhandled resource: ${resourceId}`);
			} catch (error) {
				response.setHeader('Content-Type', 'text/plain');
				// eslint-disable-next-line require-atomic-updates
				response.statusCode = 400;
				response.write(error.message);
			}

			response.end();
		});

		this.server_.on('error', error => {
			this.logger().error('Resource server:', error);
		});

		this.server_.listen(this.port_);

		enableServerDestroy(this.server_);

View on GitHub (pinned to 2654b33620)

Solutions

  1. Inspect the 400 response body — it contains the offending resourceId; cross-reference it with the noteLinks map to find its link.type.
  2. 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.
  3. Re-render/re-parse the note to rebuild noteLinks if you suspect staleness after editing.
  4. Check the note markdown for malformed or non-standard links.

Example fix

// before (app-gui.ts link handler returns false for unknown types)
//   if (link.type === 'item') { ...; return true; }
//   return false; // -> 'Unhandled resource: <id>'
// after
//   if (link.type === 'item') { ...; return true; }
//   response.statusCode = 404;
//   response.write(`Unsupported link type: ${link?.type ?? 'unknown'}`);
//   return true;
Defensive patterns

Strategy: validation

Validate before calling

const link = noteLinks[resourceId];
if (!link || !['url', 'item'].includes(link.type)) {
  response.statusCode = 404;
  response.end(`Unknown link type: ${link?.type ?? 'none'}`);
  return true; // mark handled so ResourceServer does not throw
}

Type guard

const isKnownLink = (link: unknown): link is { type: 'url' | 'item' } =>
  !!link && typeof link === 'object' &&
  (link.type === 'url' || link.type === 'item');

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12). Data as JSON: /api/errors/5c74984a6c761165. Report an issue: GitHub.