Skip to content

Do Not Use Absolute URLs for Plane Comment Image Embeds

Adityo Guni Waluyo

Image embeds in Plane API comments fail hard on absolute URLs (401). Verified pipeline: in-container helper, image-component node, raw asset_id.

TL;DR

Using absolute URLs for Plane comment images always returns 401 because assets must load through an authenticated local proxy. Instead create the comment first, upload via the in-container helper, and embed with the native image-component node using only the raw asset ID. Remember updates replace all HTML so resend full content and verify rendering in the browser not with curl.

I needed visual evidence for a QA ticket, so I embedded 34 screenshots into 8 Plane comments. My first comment I wrote by hand with an img tag pointing at the absolute URL from the WEB_URL config. The path was right, the file sat in the bucket, but the image never rendered. The Network tab showed a single red line: 401.

My first guess was permissions: maybe the MinIO bucket was not open to the public. But after re-reading how Plane serves assets, that guess collapsed. Plane is designed so images are never loaded through a public URL. Anonymous GETs to an asset URL always answer 401, and that is expected behavior, not a bug [5].

The wrong absolute-URL guess

The old flow in my notes pasted the helper's absolute URL into an img tag. It looked sensible: the URL is valid, the browser just fetches it. But the browser holds no valid Plane session for the asset endpoint, and that endpoint demands one. Same result every time: broken image, console full of 401s.

The way out restricts instead of opening. Images must load through the local proxy, and the embed method is not an img tag with a full URL but the editor's native image-component node with src holding the raw asset_id. The renderer resolves that src relative to the current origin, so the request lands on the local proxy and the public URL is never touched.

Four steps of the verified pipeline

I verified this pipeline end to end: 34 images across 8 comments, all rendering locally. Four steps, none skippable.

First, create the comment. The asset binds to the comment id, not the work item. The official comment object carries comment_html as the HTML string version of the comment, plus comment_json and an attachments list [1].

Second, upload through the in-container helper. The MCP server has no file-upload tool, so the only reliable path is a helper that copies the file into the plane-api container, creates the FileAsset row with entity type COMMENT_DESCRIPTION and comment_id set, then uploads to the bucket. The official attachment object has a comment field holding the comment id when an asset binds to a comment, and asset as the storage path or identifier [2]. The helper prints one JSON line with asset_id, asset_url, and url; only the raw asset_id goes into the embed.

Third, embed with the native editor node, not an img tag. The Tiptap image extension renders image nodes through its own HTML tag; the extension only displays, upload is handled separately [3]. The node shape Plane uses:

<image-component data-id="<unique-uuid>" src="<asset_id>" id="<unique-uuid>" width="640px" height="360px" aspectratio="1.7777777777777777" alignment="left" status="uploaded"></image-component>

data-id and id only need to be syntactically valid unique UUIDs, one per node. Never put the helper's absolute URL into src: that URL is WEB_URL-based so it shoots at the public internet, violating the local-only access rule.

Fourth, verify in two layers. Re-read the comment through MCP and confirm the HTML round-trips verbatim, then confirm in the browser that images render complete with readable dimensions. Never verify with an anonymous curl to the asset URL: the 401 there is expected noise.

Two pitfalls that ate the guessing time

First pitfall: updating comment_html replaces the whole field. The comment-update endpoint modifies an existing comment's content through the comment_html body [4]. Send new HTML containing only the image node and all old text is gone. You must fetch the original HTML verbatim first, then send it back whole with the embed appended at the end. API writes are stored verbatim; canonicalization only happens on UI save.

Second pitfall: do not fetch from page JavaScript. Same-origin fetches to the API endpoint from page JS get rejected because the session-cookie auth is not accepted. This whole pipeline is helper-script plus MCP, not browser fetch.

So if you need visual evidence in comments on a self-hosted Plane, follow the order: create the comment first, upload through the in-container helper, embed the native node with the raw asset id, re-read to verify. Dropping the absolute URL feels like a detour at first, but it is the only way that got 34 out of 34 images rendering consistently through the local proxy.

Sources

Related articles