Conversation
…s the 503 red. head.html still decides whether a site exists, so a site not yet previewed on the requested ref is refused at 404 with its document already read and discarded. an upstream that cannot be reached still reports the failure in a return value, and a preview host that throws still reaches the browser as a 500 with no body. the full suite aborts on load, because src/storage/site.js does not exist yet.
…ce means head.html decided whether a site exists, so a site not yet previewed on the requested ref was refused at 404 with its document already read and discarded. config.aem.page answers both questions in one read: whether there is such a site, and which store holds its content. content.source.url is the field helix-admin sets x-api-upgrade-available from, so it is the boolean /ping conveyed. the /ping probe and HLX_ADMIN are gone. three upstreams reported a failure by returning a value, and two swallowed one. reach() names the upstream and rethrows a typed UpstreamError, and each route entry point catches once and builds the 503 its method takes. x-error names the upstream every time, not only on the site lookup. a 401 or 403 cannot be retried, so it is not a 503. neither is a 404. the preview host and the config store fall back on all three and log the refusal, and only a status that means no answer becomes an UpstreamError. a throw that is not an UpstreamError reaches the worker boundary, which logs it and answers 500. Relates to #258
…repo pins HLX_ADMIN goes out of all three environments: its only reader was the deleted /ping probe. dev points at dev/config-shim.js on 4713, which stands in for config.aem.page so the worker runs locally without the shared secret, and .gitignore gains the glob because npm start runs --env dev and wrangler reads .dev.vars.dev before .dev.vars. the deploy job had no npm ci, so wrangler-action installed its own default, 3.90.0, which has no secrets key and would deploy past a missing HLX_CONFIG_SERVICE_TOKEN. da-admin and da-collab install and run npm run deploy instead of using the action; this matches them. node 24, above the floor wrangler and miniflare declare.
…aration
v3 installs its own wrangler 3.90.0, which has no secrets key in its config
schema, so `secrets = { required = [...] }` is ignored and a deploy without
HLX_CONFIG_SERVICE_TOKEN goes out green. v4 defaults to wrangler 4.
same two lines as #214.
…nd the head red. the head arrives with the existence answer, so one pipeline read replaces the admin scope, and the source-bus flag comes off admin.hlx.page/ping again.
…t exists green. the pipeline scope answers existence and head.html in one read, so the admin scope goes, and with it the CDN token and api key metadata the worker was reading. a write asks /ping only.
red. header-absent-on-5xx read as legacy, which sends a source-bus write to da-admin where nothing serves it back.
the header is read ahead of the status, so the edge dictionary still answers for an origin that is rate limited. a refusal without it now throws.
red. /ping answers 200 with no header for a source-bus site in exactly that window, since helix-admin reads the same config and swallows the failure.
red. it answered with the store-did-not-answer text, and no store was asked.
the store answer comes from the same config, so an outage that hides one hides the other. a wrong store cannot be walked back from, and the 503 now names the destination as undetermined rather than the store as unreachable.
Wrap the composed page's preview-host read and the store body that arrives after the status so both answer 503 with a body naming the upstream, give the metadata sheet the same deadline the site lookup has, refuse a document parse5 leaves without a body, and resolve the site before a write parses anything.
Run the real compose, csp and UE modules through the route so the nonce applyCsp mints is asserted where it has to land, on the injected UE scripts and the quick-edit import map, with no placeholder left in the response.
Pin the log line an unset HLX_CONFIG_SERVICE_TOKEN owes before the lookup goes out, that the lookup still goes out, that a set token stays quiet, and that a 401 or 403 names whether the token was there.
An unset HLX_CONFIG_SERVICE_TOKEN goes out as the string "undefined" and comes back a refusal that looks exactly like an upstream outage, so log the misconfiguration before the lookup and name the token's presence on a 401 or 403. The lookup still goes out and still fails the same way, since short-circuiting it would change behaviour where only the log was missing, and the token value never reaches either line.
Assert that a policy asking to move to a header keeps its meta in the head, carrying the rewritten nonce and neither move attribute, so the surviving element is covered rather than only the attributes being gone.
The pipeline moves a policy carrying move-to-http-header to a response header and drops the meta, while this branch keeps the meta on purpose, since frame-ancestors delivered as a header would stop the editor framing the page and is ignored in a meta element; note as well that rewriting the head here is what supersedes the preview-host head.html read on UE.
Serve the policy and its placeholders from the stand-in head, add a site on the source bus, and refuse a request that carries no token.
Composing walks the stored document as well as reading the metadata sheet, so pin that a throw from that work still reaches the boundary, and name the deadline test after what it checks.
Wrapping the whole compose step answered a bug in the worker's own tree work as a retryable preview-host outage, so the wrap now sits on the metadata read alone.
The comment named two branch commits that a squash merge would leave unreachable, so it now states the decision itself: the rewrite is what lets the config service answer head.html for the editor, and reading it from the preview host was the alternative it replaced.
A stylesheet, script or sheet the preview host cannot answer reaches the worker boundary as a bodyless 500, where every other upstream read on the branch answers 503 with Retry-After and an x-error naming what failed.
The proxy read now names the preview host, and the two handlers that call it directly answer 503 with Retry-After instead of letting the throw reach the worker boundary as a bodyless 500. The refusal carries no body, since an HTML shell would corrupt a stylesheet or a sheet; the asset paths still fall back through allSettled unchanged.
A site that needs no site token and an exchange the service refused both end with no site token and no log, so an author on an authenticated site loses the token with nothing anywhere saying why.
The non-ok branch folded a 400, a 401, a 404 and any 5xx into the same silent null the public-site answer produces, so the refusal now names the site and the status and the fail-open behaviour is unchanged.
live test on stageBranch deployed to stage. Site behind Helix auth for the run. 53 pass, 0 fail, 1 tbd. config service decides
head.html source before #270: preview host. After: config service. Checks 7 and 8 hold while the preview host answers 401 — see the resource route below. write refusals
resource route
da:401 shell
trusted origins
A trusted origin reaches the auth check and answers 401 without a bearer. An untrusted one answers 403 before the auth check. policy rewrite
nonce reaches the injected scripts
One line out of 41 means the policy and all six head scripts share one value: quick-edit
preview host
branch that does not exist
tbd
|
|
two more:
55 pass, 0 fail. The config service classifies the store the same way on stage as on production, so a source-bus site under any environment covers 54. The 405 lands before |
editor test on stageThe canvas host decides which worker is tested. Open the editor on 1. open the editor, stage canvas host2. get a stage tokenSecond tab, fetch('https://ims-na1-stg1.adobelogin.com/ims/profile/v1',
{ headers: { Authorization: 'Bearer STAGE_TOKEN' } }).then(r => console.log(r.status))
// 2003. mint the cookie, in the editor tab console, then reloadfetch('https://main--{site}--{org}.stage-ue.da.live/gimme_cookie', {
headers: { Authorization: 'Bearer STAGE_TOKEN' },
credentials: 'include'
}).then(r => r.text()).then(console.log)
// cookie setresultCanvas loads the page.
Console: no The notice confirms that |
quick-edit test on stageOpen da-live from 1. open the canvasSign in. Canvas edits and saves. 2. confirm the host runs this branch
3. check the stored document after a savefetch('https://main--{site}--{org}.stage-preview.da.live/{path}?quick-edit=on',
{ credentials: 'include' })
.then(r => r.text())
.then(t => console.log('nonce in body:', /nonce=/.test(t.split('</head>')[1] || '')))
// falseresultConsole: no bootstrap appended to no nonce in the stored document after save |
|
|
||
| // a refused stylesheet, script or sheet carries no body: nothing renders CSS, so the HTML shell a | ||
| // page gets would only corrupt what the browser is parsing. | ||
| export function resource503(error = '') { |
There was a problem hiding this comment.
There is already a get503 and a post503 helper can't these be used?
There was a problem hiding this comment.
get503 and post503 have a body and here we're sending a 503 for a resource that couldn't be fetched. I don't think sending a body makes sense here.
resource503 and head503 do the same, I'll consolidate that
| export const PREVIEW_FAILED_HTML_MESSAGE = '<html><body><h1>503: Preview host failed</h1><p>The site\'s preview host could not be read. Please retry, or contact your project admin if it persists.</p></body></html>'; | ||
|
|
||
| export const SOURCE_UNDETERMINED_MESSAGE = 'Which store holds this document could not be determined, so nothing was written. Please retry.'; | ||
| export const EDITOR_CONFIG_FAILED_HTML_MESSAGE = '<html><body><h1>503: Editor config failed</h1><p>The editor configuration for this site could not be read. Please retry, or contact your project admin if it persists.</p></body></html>'; |
There was a problem hiding this comment.
Parts of them have been here before, but should we maybe align on either sending only HTML messages or only text messages?
There was a problem hiding this comment.
that would need changes in UE though.
for GETs the message is what users see in the browser instead of whatever failed, so HTML seems right
for POSTs, UE embeds the message in problem+json so it needs to be plain-text. or at least if it's HTML we'd show HTML markup to the human in front of the screen
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
live test on production33 pass, 0 fail. the deployed worker runs this change
config service decides
CSP
writes
auth
preview host passthrough
|
Description
Reapplies #270 (reverted in #275) + the CSP rewrite that makes it safe.
#270 got
head.htmlfrom the config service instead of the preview host, so CSP-metas with 'nonce-aem' made it to the page and were enforced -> editor could not instrument the page loaded to the canvasda-universal now rewrites the CSP when it builds the page
before
/head.htmlto a response header thatgetAEMHtmlthrows away, so the page had no CSP at all{ref}--{site}--{org}.aem.page/head.htmlwithout a site token, so the editor got a page with an empty headnow
head.htmland the store'nonce-aem'in the policy and onhead.html' elements, put it on the scripts we inject (UE scaffold and quick-edit import map)<meta>and is not moved to a response header, soframe-ancestors,sandboxandreport-uristay unused. (frame-ancestors 'self'prevent the page loading in the editor)require-trusted-types-foris dropped from the policy. neither a nonce nor the editor's DOM writes can satisfy it.'nonce-aem'is left as isisUEalso recognises the configured local host to make the editor path reachable in local developmentthe worker names the upstream that failed
The worker answers 503 with
Retry-Afterand anx-errornaming the upstream that failed: preview host, content store, editor config or site lookup. The worker answers a bodyless 500 for a throw in its own tree work. Tests pin both..css,.js,.json, font and.plain.htmlrequestsHLX_CONFIG_SERVICE_TOKENwas set when the config service answers 401 or 403, separating a rotated token from an outagelocal development
The stand-in in
dev/serves ahead.htmlwith the policy and the placeholders, holds one site per store kind, and answers 401 without a token, matching the config service. A local run therefore exercises the nonce rewrite, the trusted-types strip and both store branches.tested
Unit — 495 tests.
src/render/csp.js,src/routes/da-admin.js,src/storage/site.js,src/routes/cookie.jsandsrc/utils/upstream.jsat 100% statements; 94.17% over all files.Live on stage — 55 checks over HTTP, 0 failures, site behind Helix auth for the run.
The preview host answered 401 for all three direct reads without a site token:
/index.html,styles.css,component-definition.json. The worker answered 200 on the page it builds, in the same run, with all fourhref/srcvalues declared byhead.htmlpresent. head.html source before #270: preview host. After: config service.One nonce per request on the policy and on all six head scripts, four of them injected by the editor. Second request gives a different value.
'nonce-aem'gone.require-trusted-types-forandmove-to-http-headerdeclared at source, absent from the served policy, meta kept. No CSP response header. Quick-edit import map injected first in the document, same nonce. Plain preview host not UE-instrumented, policy still rewritten.200 without policy and nonce for a non-existing branch — the
headHtml ?? ''path.404 with its own body for an unknown site, and a bare
.mp4on it is a 404 with no HTML. POST refused in order: non-html extension, non-html part content-type and missingdatapart each 415 ahead of the site lookup, lookup itself 404, none with aRetry-After. All elevenTRUSTED_ORIGINSentries and near-misses landed on the correct side, including theexperience-stage.adobe.comentry this PR adds.da:401recovery shell served, and HEAD without authorization is a bodyless 401 instead of the shell.In the editor — the canvas loads the page and the editor initialises.
scripts/aem.jsandscripts/scripts.jshave the minted nonce and execute. They then request the block CSS and JS,nav.plain.html,footer.plain.htmlanddelayed.js— requests the browser only makes if it ran the two scripts, so it accepted the nonce.POST /configurationanswers 200 with the page url. The editor readscomponent-definition.json,component-models.jsonandcomponent-filters.json, and the component tree fills.Console: no
Refused to execute, noRefused to load, noviolates. One notice:The notice states the behaviour the rewrite depends on.
applyCspleaves the policy in a<meta>instead of moving it to a response header, so the browser dropsframe-ancestorsand the editor frames the page.Open — browser only: editor boot and component tree, CSP violations in console,frame-ancestorsnot enforced from a meta, nonce accepted instead of only present in the bytes. Two HTTP checks also open: 405 for a source-bus write, and 415 for a body-less document, the only check that runs past a successful lookup.Quick-edit —
https://{branch}--da-live--adobe.aem.page/canvas#/{org}/{site}/{path}points the canvas atstage-preview.da.liveand signs in against stage IMS.The canvas edits and saves. Console: no
Refused to execute, noRefused to load, noFailed to resolve module. The import map resolvesda-litandda-y-wrapperfrom da.live. The bootstrap the worker appends toscripts/scripts.jsruns and callsaddImportmap; the browser keeps the map the worker injected first and drops the duplicate rules:A save leaves no nonce in the stored document.