From 0bd684afabb72189595f40990f08afa7fee84955 Mon Sep 17 00:00:00 2001 From: Adam Tan Date: Sat, 11 Apr 2026 21:38:29 +0800 Subject: [PATCH] fix: prepend /wiki context path in toAbsoluteUrl for Cloud attachment downloads The REST API returns _links.download as a path relative to the Confluence context root (e.g. /download/attachments//?version=...). On Atlassian Cloud the context root is /wiki, so toAbsoluteUrl must prepend webUrlPrefix before handing the URL to axios. Previously it went straight through buildUrl, producing https:///download/... which returns 404 on Cloud. This made `confluence attachments --download` completely unusable against any Cloud instance while listing still worked. The fix reuses the existing webUrlPrefix (already set on line 38) and guards against double-prefixing if the API ever starts returning paths that already include /wiki. Server/DC behavior is unchanged because webUrlPrefix is an empty string when apiPath does not start with /wiki/. Adds three targeted tests covering Cloud download URL construction, double-prefix guard, and Server/DC backward compatibility. --- lib/confluence-client.js | 11 ++++++++++- tests/confluence-client.test.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/confluence-client.js b/lib/confluence-client.js index 2d106f6..b5f51e8 100644 --- a/lib/confluence-client.js +++ b/lib/confluence-client.js @@ -2078,7 +2078,16 @@ class ConfluenceClient { return pathOrUrl; } - return this.buildUrl(pathOrUrl); + // Prepend context path (e.g. /wiki on Atlassian Cloud) when the API returns + // a path relative to the Confluence context root. Without this, URLs such as + // attachment download links (_links.download → "/download/attachments/...") + // get built as https:///download/... instead of + // https:///wiki/download/..., which returns 404 on Cloud. + const prefix = this.webUrlPrefix || ''; + const needsPrefix = prefix && !pathOrUrl.startsWith(`${prefix}/`); + const withPrefix = needsPrefix ? `${prefix}${pathOrUrl}` : pathOrUrl; + + return this.buildUrl(withPrefix); } parseNextStart(nextLink) { diff --git a/tests/confluence-client.test.js b/tests/confluence-client.test.js index 237bc39..74fe502 100644 --- a/tests/confluence-client.test.js +++ b/tests/confluence-client.test.js @@ -105,6 +105,39 @@ describe('ConfluenceClient', () => { }); expect(httpClient.toAbsoluteUrl('https://cdn.example.com/file.pdf')).toBe('https://cdn.example.com/file.pdf'); }); + + test('toAbsoluteUrl prepends /wiki context path on Atlassian Cloud', () => { + const cloudClient = new ConfluenceClient({ + domain: 'test.atlassian.net', + token: 'token', + apiPath: '/wiki/rest/api' + }); + // _links.download from the API is relative to the Confluence context root, + // so on Cloud (apiPath starts with /wiki/) we must prepend /wiki. Otherwise + // the resulting URL hits https:///download/... and returns 404. + expect(cloudClient.toAbsoluteUrl('/download/attachments/123/file.png?version=1&modificationDate=1700000000000&cacheVersion=1&api=v2')) + .toBe('https://test.atlassian.net/wiki/download/attachments/123/file.png?version=1&modificationDate=1700000000000&cacheVersion=1&api=v2'); + }); + + test('toAbsoluteUrl does not double-prepend /wiki when path already starts with it', () => { + const cloudClient = new ConfluenceClient({ + domain: 'test.atlassian.net', + token: 'token', + apiPath: '/wiki/rest/api' + }); + expect(cloudClient.toAbsoluteUrl('/wiki/download/attachments/123/file.png')) + .toBe('https://test.atlassian.net/wiki/download/attachments/123/file.png'); + }); + + test('toAbsoluteUrl leaves Server/DC paths untouched (no webUrlPrefix)', () => { + const serverClient = new ConfluenceClient({ + domain: 'confluence.example.com', + token: 'token', + apiPath: '/rest/api' + }); + expect(serverClient.toAbsoluteUrl('/download/attachments/123/file.png')) + .toBe('https://confluence.example.com/download/attachments/123/file.png'); + }); }); describe('api path handling', () => {