Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f057144
test: cover Helix 6 source routing
benpeter Jul 28, 2026
bf254ed
test: cover ping timeout recovery
benpeter Jul 28, 2026
1eb95c9
feat: route source-bus traffic through Helix 6
benpeter Jul 28, 2026
d67853a
test: require fresh source routing probes
benpeter Jul 28, 2026
3449993
refactor: remove source routing cache (KISS)
benpeter Jul 28, 2026
f266c20
test: clean routing transition setup
benpeter Jul 28, 2026
309cc2d
test: cover workerd probe failures
benpeter Jul 28, 2026
e5d5146
fix: handle all source probe failures
benpeter Jul 28, 2026
91c565e
test: align unknown source writes
benpeter Jul 28, 2026
237e555
fix: align unknown source routing
benpeter Jul 28, 2026
df3c112
test: preserve source read failures
benpeter Jul 28, 2026
e52569b
fix: preserve source read failures
benpeter Jul 28, 2026
65c2cd0
test: route uncertain writes by backend evidence
benpeter Jul 28, 2026
7b65a91
fix: resolve uncertain writes by backend evidence
benpeter Jul 28, 2026
cbba3ba
test: preserve fulfilled source HEAD evidence
benpeter Jul 28, 2026
29a7757
fix: keep fulfilled HEAD evidence
benpeter Jul 28, 2026
3d12ed8
test: reject source routing warnings
benpeter Jul 29, 2026
0222c9f
fix: remove source routing warnings
benpeter Jul 29, 2026
a70838a
test: reject source bus request logs
benpeter Jul 29, 2026
ecf9410
fix: remove source bus request logs
benpeter Jul 29, 2026
6f5fc11
Merge remote-tracking branch 'origin/main' into hlx6-ping
benpeter Jul 29, 2026
74b1399
fix(dry): don't repeat default method
benpeter Jul 29, 2026
0cc671e
refactor: simplify da-admin source fetch
benpeter Jul 29, 2026
12e6198
refactor: return source HEAD response directly
benpeter Jul 29, 2026
42d21ba
refactor: don't repeat default method
benpeter Jul 29, 2026
a6b7579
test: require source bus request logs
benpeter Jul 29, 2026
d769ad6
fix: align source bus request logging
benpeter Jul 29, 2026
339fb37
test: preserve explicit source extensions
benpeter Jul 29, 2026
d25d4c1
fix: preserve explicit source extensions
benpeter Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 121 additions & 18 deletions src/routes/da-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,53 @@ import { BRANCH_NOT_FOUND_HTML_MESSAGE, DEFAULT_HTML_TEMPLATE, UNAUTHORIZED_HTML
import { getSiteConfig } from '../storage/config.js';
import { restoreAbsoluteImages } from '../render/rewrite-images.js';

const AEM_API = 'https://api.aem.live';
const HLX_ADMIN = 'https://admin.hlx.page';
const UPGRADE_PROBE_TIMEOUT = 2 * 1000;

function aemApiSourceUrl(org, site, path) {
return `${AEM_API}/${org}/sites/${site}/source${path}`;
}

async function probeHlx6(org, site) {
const pingUrl = `${HLX_ADMIN}/ping/${org}/${site}`;
try {
const response = await fetch(pingUrl, {
signal: AbortSignal.timeout(UPGRADE_PROBE_TIMEOUT),
});
if (response.status !== 200) {
return undefined;
}
return response.headers.get('x-api-upgrade-available') === 'true';
} catch {
return undefined;
}
}

/**
* Resolves the write backend after the site probe fails.
* Both backends are HEADed concurrently. A source-bus 200 selects api.aem.live.
* Every other result selects da-admin, including a missing document or failed HEAD.
*
* @returns {Promise<boolean>} true for api.aem.live, false for da-admin
*/
async function resolveUncertainWriteBackend({
Comment thread
benpeter marked this conversation as resolved.
org, site, sourcePath, authToken, env,
}) {
const headers = new Headers({ Authorization: authToken });
const aemUrl = aemApiSourceUrl(org, site, sourcePath);
const daUrl = new URL(`/source/${org}/${site}${sourcePath}`, env.DA_ADMIN);

const [aemResult] = await Promise.allSettled([
fetch(aemUrl, { method: 'HEAD', headers }),
env.daadmin.fetch(daUrl, { method: 'HEAD', headers }),
]);
if (aemResult.status === 'fulfilled' && aemResult.value.status === 200) {
return true;
}
return false;
}

async function getFileBody(data) {
const text = await data.text();
return { body: text, type: data.type };
Expand Down Expand Up @@ -90,11 +137,21 @@ export async function daSourceGet({ req, env, daCtx }) {
const headers = new Headers();
headers.set('Authorization', authToken);

const hlx6Promise = probeHlx6(org, site);

if (ext !== 'html') {
/*
for non-HTML files, simply proxy the request without processing
and ensure that extensions are not duplicated
*/
const hlx6 = await hlx6Promise;
if (hlx6) {
const sourceUrl = aemApiSourceUrl(org, site, path);
console.log(`-> ${sourceUrl}`);
const response = await fetch(sourceUrl, { headers });
console.log(`<- ${sourceUrl}. ${response.status} ${response.statusText}`, { status: response.status, statusText: response.statusText });
return response;
}
const adminUrl = new URL(`/source/${org}/${site}${path}`, env.DA_ADMIN);
console.log(`-> ${adminUrl.toString()}`);
const response = await env.daadmin.fetch(adminUrl, { method: 'GET', headers });
Expand All @@ -104,7 +161,10 @@ export async function daSourceGet({ req, env, daCtx }) {

// get the AEM parts (head.html)
const aemCtx = getAemCtx(env, daCtx);
const headHtml = await getAEMHtml(aemCtx, '/head.html');
const [headHtml, hlx6] = await Promise.all([
getAEMHtml(aemCtx, '/head.html'),
Comment thread
benpeter marked this conversation as resolved.
hlx6Promise,
]);
if (!headHtml) {
// quick-edit still needs a working shell (with the import map) so the editor
// can load into this page, even when the AEM branch doesn't exist yet.
Expand All @@ -114,24 +174,30 @@ export async function daSourceGet({ req, env, daCtx }) {
return get404(BRANCH_NOT_FOUND_HTML_MESSAGE);
}
Comment thread
benpeter marked this conversation as resolved.

// get the content from DA admin
const adminUrl = new URL(
`/source/${org}/${site}${path}.${ext}`,
env.DA_ADMIN,
);
let sourceResp;
if (hlx6) {
const sourceUrl = aemApiSourceUrl(org, site, `${path}.${ext}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but path already has the ext, no?

console.log(`-> ${sourceUrl}`);
sourceResp = await fetch(sourceUrl, { headers });
console.log(`<- ${sourceUrl}. ${sourceResp.status} ${sourceResp.statusText}`, { status: sourceResp.status, statusText: sourceResp.statusText });
} else {
const adminUrl = new URL(
`/source/${org}/${site}${path}.${ext}`,
env.DA_ADMIN,
);
console.log(`-> ${adminUrl.toString()}`);
const daAdminResp = await env.daadmin.fetch(adminUrl, { headers });
console.log(`<- ${adminUrl.toString()}. ${daAdminResp.status} ${daAdminResp.statusText}`, { status: daAdminResp.status, statusText: daAdminResp.statusText });
sourceResp = daAdminResp;
}

// eslint-disable-next-line no-param-reassign
req = new Request(adminUrl, {
method: 'GET',
headers,
});
console.log(`-> ${adminUrl.toString()}`);
const daAdminResp = await env.daadmin.fetch(req);
console.log(`<- ${adminUrl.toString()}. ${daAdminResp.status} ${daAdminResp.statusText}`, { status: daAdminResp.status, statusText: daAdminResp.statusText });
if (sourceResp.status !== 200 && sourceResp.status !== 404) {
return sourceResp;
}

// use the stored content when available, otherwise fall back to a template
const bodyHtml = daAdminResp && daAdminResp.status === 200
? await daAdminResp.text()
const bodyHtml = sourceResp.status === 200
? await sourceResp.text()
: await getPageTemplate(env, daCtx, aemCtx, headHtml);

// compose the page the same way for every request type
Expand Down Expand Up @@ -174,6 +240,15 @@ export async function daSourceHead({ env, daCtx }) {
headers.set('Authorization', authToken);

const adminPath = ext !== 'html' ? path : `${path}.${ext}`;
const hlx6 = await probeHlx6(org, site);
if (hlx6) {
const sourceUrl = aemApiSourceUrl(org, site, adminPath);
console.log(`-> HEAD ${sourceUrl}`);
const response = await fetch(sourceUrl, { method: 'HEAD', headers });
console.log(`<- HEAD ${sourceUrl}. ${response.status} ${response.statusText}`, { status: response.status, statusText: response.statusText });
return response;
}

const adminUrl = new URL(`/source/${org}/${site}${adminPath}`, env.DA_ADMIN);
console.log(`-> HEAD ${adminUrl.toString()}`);
const response = await env.daadmin.fetch(adminUrl, { method: 'HEAD', headers });
Expand Down Expand Up @@ -205,14 +280,42 @@ export async function daSourcePost({ req, env, daCtx }) {

minifyWhitespace(bodyNode);

const bodyContent = toHtml(bodyNode);
// daCtx.path contains explicit file extensions; append only inferred HTML.
const sourcePath = ext !== 'html' ? path : `${path}.${ext}`;
let hlx6 = await probeHlx6(org, site);
if (hlx6 === undefined) {
hlx6 = await resolveUncertainWriteBackend({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this check. if it's not hlx6, then it's not hlx6...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined means that the probe failed, in which case resolveUncertainWriteBackend will HEAD both backends and use api.aem.live if the document exists there, otherwise da-admin. I'll add JSDoc to explain, yes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but if the probe fails, why should the HEAD request then be a good alternative?

and for a new file, resolveUncertainWriteBackend it will also return false, because its a 404.

org,
site,
sourcePath,
authToken,
env,
});
}
if (hlx6) {
const sourceUrl = aemApiSourceUrl(org, site, sourcePath);
const headers = {
Authorization: authToken,
'Content-Type': 'text/html',
};
console.log(`-> ${sourceUrl}`);
const response = await fetch(sourceUrl, {
method: 'POST',
body: bodyContent,
headers,
});
console.log(`<- ${sourceUrl}. ${response.status} ${response.statusText}`, { status: response.status, statusText: response.statusText });
return response;
}

// create new POST request with the body content
const body = new FormData();
const bodyContent = toHtml(bodyNode);
const data = new Blob([bodyContent], { type: 'text/html' });
body.set('data', data);
const headers = { Authorization: authToken };
const adminUrl = new URL(
`/source/${org}/${site}${path}.${ext}`,
`/source/${org}/${site}${sourcePath}`,
env.DA_ADMIN,
);
// eslint-disable-next-line no-param-reassign
Expand Down
10 changes: 10 additions & 0 deletions test/routes/da-admin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ describe('daSourceGet', () => {

// record which composition / instrumentation calls happen and with what
let calls;
let originalFetch;

beforeEach(() => {
originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response('', { status: 200 });
});

afterEach(() => {
globalThis.fetch = originalFetch;
});

const mockDaSourceGet = async (overrides = {}) => {
// 'headHtml' in overrides (rather than a destructured default) so passing
Expand Down
Loading