feat(cfl): add page export to PDF - #481
Draft
piekstra wants to merge 2 commits into
Draft
Conversation
Exporting a page to PDF was the one thing that sent a workflow back to the Confluence web UI, which matters when the page is a document that has to be sent to someone outside the site. Confluence Cloud publishes no REST endpoint for this. What it does have is the action its own UI navigates to, which both starts a server-side render and returns a page carrying the task identifier as metadata, plus a progress endpoint that reports the task and finally names the rendered document. `page export` drives those three legs: start, poll, download. Details worth knowing at the call site: - The XSRF header is what separates an accepted start request from a 403. - Confluence declares which generation of the progress endpoint served the request, and that also decides whether the task result is the document URL or a URL yielding one. Both are handled; neither is guessed. - The document is handed off to a media host with a signed URL that carries its own access, so the credential is sent only to the configured site and never to whatever host the task names. - A rejected credential is answered with a sign-in page under HTTP 200, so the download is checked for the PDF header rather than trusting the status. Progress goes to stderr and the success block to stdout, so redirecting stdout captures the result and nothing else. Without --output-file the page title names the file, reduced to a single path element.
The export drives an undocumented Confluence flow that Atlassian has moved before without notice, so the catalog is where a regression in it would be caught. Also records that bearer auth is unverified for this command rather than leaving the gap implied: the export goes through a web action rather than a REST endpoint, and whether the gateway proxies that action has not been established.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
cfl page export <page-id>, which writes a Confluence page out as a PDF.Everything else about a page is reachable from the CLI, but exporting one was
not, so any workflow that ends in "send this page to someone as a document" had
to stop and go click Export to PDF in the web UI. This closes that gap.
Why it works the way it does
Confluence Cloud publishes no REST endpoint for PDF export. The request has
been open since 2018 (CONFCLOUD-61557,
still Gathering Interest), and the Atlassian KB article that describes a
302straight to a PDF is scoped to Data Center and does not apply here. Thev1andv2API groups have no export surface at all. I confirmed both againsta live Cloud site rather than taking the docs' word for it:
GET /wiki/api/v2/pages/{id}/export404GET /wiki/rest/api/content/{id}/export404GET /wiki/rest/api/content/{id}/exportpdf404What does exist is the flow the Confluence UI itself uses, and it works with the
API-token auth
cflalready has. It has three legs:GET /wiki/spaces/flyingpdf/pdfpageexport.action?pageId=<id>starts aserver-side render and returns an HTML progress page. The task identifier is
published only as
<meta name="ajs-taskId">inside that page, which is whythis leg parses HTML. A sibling
ajs-isV3declares which progress endpointserved the request.
GET /wiki/api/v2/pdfexporttask/progress/<taskId>reports{progress, state, result}untilstatereachesSUCCEEDED.resultnames the rendered document, which is then downloaded.Four behaviors here are not obvious and each one is load-bearing:
X-Atlassian-Token: no-checkthe start request is refused with
403; with it,200. I hit exactly thiswhile building it.
is the only thing that separates them. A bogus action path returns
404whilepdfpageexport.actionreturns200, which is the control that establishes theroute is real rather than being swallowed by the SPA router.
inverts between them. The document is normally handed to a media host with a
signed URL carrying its own access, where sending the Atlassian credential is
both wrong and a credential leak to another host. A site-relative result needs
the credential. Sending it is therefore decided by where the URL points.
status code cannot catch that, so the download is verified to start with
%PDF-and fails withErrExportNotPDFotherwise, rather than writing an HTMLpage to disk under a
.pdfname.Both progress-endpoint generations are supported because the page declares which
one applies; the pre-v3 shape adds one indirection (the result addresses a
resource whose body is the document URL). Neither shape is guessed: both come
from the export script Confluence itself ships.
Verification
Run against a live Confluence Cloud site with the ordinary API-token config:
Guard paths, also run live:
make checkis green (tidy, lint, test, build).Notes for review
progress endpoint changed in April 2026, breaking the recipes in the wild).
That is an argument for the failure modes above being explicit rather than for
avoiding the path, since it is the only one that produces a PDF at all. The
alternative considered was rendering the page body locally, which would produce
a document that does not match what Confluence's own export produces.
attachment downloadprecedent (-O/--output-file,-f/--force) and the mutation contract inOUTPUT_SPEC.md: a success summaryplus follow-up fields, progress on stderr so stdout stays clean for scripting.
--formatcurrently accepts onlypdf. It is there so the closed set isstated and rejected loudly rather than a second format being bolted on later
by positional convention.
api/testdata/.Docs
tools/cfl/README.md, rootREADME.md,internal/cmd/OUTPUT_SPEC.md,skills/Confluence/CliReference.md,skills/Confluence/Workflows/ManagePage.md,and the cfl changelog.
tools/cfl/integration-tests.mdgains apage exportsection. That catalog iswhere a break in the undocumented flow would actually be caught, and it also
records the one thing I could not establish: bearer auth is unverified for
this command. The export goes through a web action rather than a REST
endpoint, and whether the
api.atlassian.comgateway proxies that action hasnot been tested. I would rather state that than let it be assumed.