Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
c31263d
test: a missing site is the 404, and an upstream that cannot answer i…
benpeter Aug 11, 2026
4686062
fix: ask the config service what exists, and stop guessing what silen…
benpeter Aug 11, 2026
b33f8cc
chore: point at the config service, and deploy with the wrangler the …
benpeter Aug 11, 2026
e5a14ce
ci: bump wrangler-action to v4, which knows the required-secrets decl…
benpeter Aug 11, 2026
0a29287
test: the page head comes from the config service, and a refused read…
benpeter Aug 12, 2026
1ffeb81
fix: read head.html from the config service, not the preview host
benpeter Aug 12, 2026
bd96daa
test: the store the lookup could not name outranks a failed head read
benpeter Aug 12, 2026
01082cd
fix: report the store the lookup could not name before a failed head …
benpeter Aug 12, 2026
fd17d55
fix: declare the config service token in the dev env, so .dev.vars.de…
benpeter Aug 12, 2026
ce03d15
test: pin the head read to the html GET, and keep the preview host 50…
benpeter Aug 12, 2026
403c160
test: /ping decides the store, the config service decides existence a…
benpeter Aug 12, 2026
4be8875
fix: ask /ping which store holds the site, and the config service wha…
benpeter Aug 12, 2026
dfe6d96
test: a /ping that refuses is no answer, not a legacy site
benpeter Aug 12, 2026
5f0ff03
fix: refuse when /ping cannot answer, instead of reading it as legacy
benpeter Aug 12, 2026
bd5bc35
test: a save is refused when the config service cannot answer
benpeter Aug 12, 2026
a36dab8
test: a save refused on the site lookup says the store is undetermined
benpeter Aug 12, 2026
47d66fa
fix: refuse a save when the site lookup cannot answer
benpeter Aug 12, 2026
b490ca3
test: the store comes from the config service content source, not /ping
benpeter Aug 13, 2026
c2162a4
fix: take the store from the config service content source, and drop …
benpeter Aug 13, 2026
e3e9ecf
chore: name the two config scopes, not /ping, in the comments and tests
benpeter Aug 13, 2026
dc0fcec
test: a write asks the store lookup only
benpeter Aug 14, 2026
11ae458
fix: ask one lookup on a write, since the write never reads the other…
benpeter Aug 14, 2026
46b5c5f
test: drop the knobs a write no longer reaches, and name what the ass…
benpeter Aug 14, 2026
81c566c
test: drop the site lookup stub from the write case, which no longer …
benpeter Aug 14, 2026
c646d21
test: cover the cookie route, and point the exchange at the source bu…
benpeter Aug 14, 2026
8c01ab9
fix: exchange the site token on the source bus api, from env
benpeter Aug 14, 2026
708c0d1
test: one config read answers existence, head.html and the store
benpeter Aug 14, 2026
b8b63ee
fix: take existence, head.html and the store from one config read
benpeter Aug 14, 2026
2bc12f4
test: a config that names no content source reads as legacy, and says so
benpeter Aug 14, 2026
461236e
fix: read a config with no content source as legacy, and warn
benpeter Aug 14, 2026
39b5730
chore: plainer wording for the config read in the readme
benpeter Aug 14, 2026
f7de96e
test: pin the ctx get.js hands to getCookie
benpeter Aug 14, 2026
0fcd363
chore: say what throws an UpstreamError
benpeter Aug 14, 2026
fba8ff6
fix: a 503 heading says the read failed, not that the upstream was un…
benpeter Aug 14, 2026
d9d9df2
refactor: name the 503 path for a failed read, not an unreachable ups…
benpeter Aug 14, 2026
f0fd2bd
test: a write is refused for a site the config service does not know
benpeter Aug 14, 2026
3cafda2
fix: refuse a write for a site the config service does not know
benpeter Aug 14, 2026
46ed7ec
test: a failed site config read says so, instead of naming the store
benpeter Aug 14, 2026
411ac04
fix: name the site config in its own 503 body
benpeter Aug 14, 2026
1e73ef3
fix: ignore the wrangler build dir the dev shim creates under dev/
benpeter Aug 14, 2026
62ec448
test: say what the one lookup answers, and drop the knob for the prob…
benpeter Aug 14, 2026
e473c88
chore: drop getOrgConfig, which nothing imports
benpeter Aug 14, 2026
6bd7be1
test: the editor config names itself when it fails
benpeter Aug 14, 2026
e96aae5
fix: name the editor config read for what it reads
benpeter Aug 14, 2026
c2268bc
chore: clarify naming
benpeter Aug 17, 2026
52637bd
fix: keep latin-1 characters in x-error instead of blanking them
benpeter Aug 17, 2026
c2364ea
fix: finish the withUpstream rename in da-admin, the branch did not load
benpeter Aug 17, 2026
6f2363f
test: causeOf keeps latin-1 and blanks the controls
benpeter Aug 17, 2026
be3a7bb
fix: blank the c1 controls, invisible in x-error
benpeter Aug 17, 2026
8a315e8
Merge remote-tracking branch 'origin/main' into headhtml
benpeter Aug 17, 2026
96f3560
fix: ask the config service whether a site exists, not head.html (#270)
benpeter Aug 18, 2026
fe2d7ac
test: on UE, head.html comes from the preview host
benpeter Aug 18, 2026
09c3f5f
fix: on UE, read head.html from the preview host
benpeter Aug 18, 2026
4d71484
Merge remote-tracking branch 'origin/stage' into headue
benpeter Aug 18, 2026
0a3da6c
fix: trust experience-stage.adobe.com, for a first-class stage UE entry
benpeter Aug 19, 2026
e218828
test: cover composed CSP nonce forwarding
benpeter Aug 20, 2026
03b2ad7
fix: apply composed CSP nonces
benpeter Aug 20, 2026
ef872c8
test: require one composed head source
benpeter Aug 20, 2026
aa1dd76
fix: use the config-service head
benpeter Aug 20, 2026
2d096ed
test: cover local UE host detection
benpeter Aug 20, 2026
3466393
fix: detect the local UE host
benpeter Aug 20, 2026
c141198
test: keep CSP nonces out of author content
benpeter Aug 20, 2026
9f77783
fix: scope composed CSP handling to head
benpeter Aug 20, 2026
fc12c04
test: pin the composed read and the write guards
benpeter Aug 21, 2026
53e0128
fix: name the upstream the composed read failed on
benpeter Aug 21, 2026
6375ea3
test: pin the route's CSP nonce ordering
benpeter Aug 21, 2026
48fcfcd
test: separate a missing config service token from an outage
benpeter Aug 21, 2026
5de6215
fix: say when the config service token is the cause
benpeter Aug 21, 2026
4e2d4fb
test: pin the CSP meta that stays in the document
benpeter Aug 21, 2026
439ad03
chore: record why the CSP meta stays in the document
benpeter Aug 21, 2026
15466d5
fix: make the local stand-in exercise the composed page
benpeter Aug 21, 2026
7400ab3
test: pin a throw from composing as the worker's own bug
benpeter Aug 21, 2026
5dba1a8
fix: name the preview host on the read, not on composing
benpeter Aug 21, 2026
6c91218
chore: record the head source decision without commit shas
benpeter Aug 21, 2026
3c4e76d
test: pin the proxy read to the same upstream taxonomy
benpeter Aug 21, 2026
af14e2e
fix: refuse a resource read the preview host could not answer
benpeter Aug 21, 2026
d50e1e9
test: separate a refused site token exchange from a public site
benpeter Aug 21, 2026
d7da233
fix: say which status refused the site token exchange
benpeter Aug 21, 2026
e109a73
Merge remote-tracking branch 'origin/main' into headue
benpeter Aug 21, 2026
730a9b2
refactor: fold resource503 into empty503, one body-less 503 helper
benpeter Aug 25, 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
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ jobs:

- name: Deploy to Cloudflare Workers (production)
if: github.ref_name == 'main'
uses: cloudflare/wrangler-action@v3
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

- name: Deploy to Cloudflare Workers (stage)
if: github.ref_name == 'stage'
uses: cloudflare/wrangler-action@v3
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,29 @@ Prerequisites:

This worker performs all content operations via [da-admin](https://github.com/adobe/da-admin). For local development, you will also need to check out and run da-admin locally.

One read of config.aem.page, pipeline scope, answers existence, head.html and `contentSource`. Its url names the store.

The config service needs a shared secret, so local development points at `dev/lookup-shim.js` instead. Add the org and site to its `SITES` table. A site missing from the table is answered 404. A source url on api.aem.live reads as source-bus, and the table ships one site of each kind, `org/site` and `org/sourcebus`, so both branches can be driven locally.

To run da-universal locally:

1. Clone this repo to your computer.
1. Run `npm install`
1. Use `npx wrangler login` if not done before. Walk through the steps in browser.
1. In a terminal, run `npm run dev` this repo's folder.
1. Put `HLX_CONFIG_SERVICE_TOKEN="local"` in `.dev.vars.dev`, which is gitignored.
1. In a terminal, run `npm run dev:lookups` to start the stand-in lookups on port 4713.
1. In a second terminal, run `npm run dev` in this repo's folder.
1. The da-ue service API is available via https://localhost:4712

The stand-in does not read the token's value, only that there is one. Without it the lookup goes out as the string `undefined` and comes back 401, the way the real service refuses it, and the worker logs that it is the one at fault.

`npm run dev` sets `UE_HOST` to localhost:4712, so https://localhost:4712 serves the UE-instrumented page rather than the composed page as-is, and points `urn:adobe:aue:config:service` at https://localhost:8000. A Universal Editor service has to be running there for that page to open in the editor.

with the shared secret, use `npm run dev` at the real services instead of the stand-in. Put `HLX_CONFIG_SERVICE_TOKEN="<token>"` in `.dev.vars.dev`, which is gitignored, and run `npm run dev -- --var HLX_CONFIG_SERVICE:https://config.aem.page`.

### Run on stage

You can deploy da-universal on Cloudflare stage via `npm deploy:stage` to test it in a real worker environment.
You can deploy da-universal on Cloudflare stage via `npm run deploy:stage` to test it in a real worker environment.

## Customer documentation
https://docs.da.live/developers/reference/universal-editor
57 changes: 57 additions & 0 deletions dev/lookup-shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

// stands in for config.aem.page, which needs a shared secret. one read of the pipeline scope
// answers whether the site exists, its head.html and which store holds it. a site in SITES exists,
// and one with a source url on api.aem.live is source-bus
const SITES = {
'org/site': 'https://content.da.live/org/site/',
'org/sourcebus': 'https://api.aem.live/org/sites/sourcebus/source/',
};

// what the code bus has at {owner}/{repo}/{ref}/head.html, which the pipeline scope answers with.
// the policy and the placeholders are what applyCsp keys on, so a page served locally exercises
// the nonce rewrite, the trusted-types strip and the move-to-http-header deletion
const HEAD_HTML = '<meta http-equiv="Content-Security-Policy" move-to-http-header="true" content="script-src \'nonce-aem\' \'strict-dynamic\'; style-src \'nonce-aem\'; require-trusted-types-for \'script\'">\n<link rel="stylesheet" href="/styles/styles.css" nonce="aem"/>\n<script src="/scripts/scripts.js" type="module" nonce="aem"></script>\n';

export default {
async fetch(req) {
const url = new URL(req.url);

// an unset HLX_CONFIG_SERVICE_TOKEN reaches the header as the string "undefined", and the
// real service refuses that the same way it refuses no header at all
const token = req.headers.get('x-access-token');
if (!token || token === 'undefined') {
return new Response('', { status: 401, headers: { 'x-error': 'missing x-access-token.' } });
}

const [ref, site, org] = (url.pathname.split('/')[1] ?? '').split('--');
if (!org || !site) {
return new Response('', { status: 400, headers: { 'x-error': 'invalid rso path parameter.' } });
}

const source = SITES[`${org}/${site}`];
if (!source) {
return new Response('', { status: 404, headers: { 'x-error': 'config not found.' } });
}

// both stores are `type: markup`, so only the url separates them
const body = JSON.stringify({
ref,
site,
org,
head: { html: HEAD_HTML },
contentSource: { type: 'markup', url: source },
});
return new Response(body, { status: 200, headers: { 'content-type': 'application/json' } });
},
};
9 changes: 9 additions & 0 deletions dev/lookup-shim.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# the stand-in gets its own config, so wrangler dev does not read wrangler.toml: no daadmin
# binding to connect, and no HLX_CONFIG_SERVICE_TOKEN to warn about
name = "da-ue-lookup-shim"
main = "lookup-shim.js"
compatibility_date = "2023-11-21"

[dev]
port = 4713
inspector_port = 9234
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { defineConfig, globalIgnores } from '@eslint/config-helpers';
import { recommended, source, test } from '@adobe/eslint-config-helix';

export default defineConfig([
globalIgnores(['.vscode/*', '.wrangler/*', 'coverage/*']),
globalIgnores(['.vscode/*', '**/.wrangler/**', 'coverage/*']),
{
languageOptions: {
...recommended.languageOptions,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"deploy": "wrangler deploy",
"deploy:stage": "wrangler deploy --env stage",
"dev": "wrangler dev --local-protocol https --env dev",
"dev:lookups": "wrangler dev -c dev/lookup-shim.toml",
"start": "wrangler dev --local-protocol https --env dev",
"test": "c8 mocha --spec=test/**/*.test.js",
"lint": "eslint ."
Expand Down
23 changes: 18 additions & 5 deletions src/handlers/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { get404, getRobots } from '../responses/index.js';
import { empty503, get404, getRobots } from '../responses/index.js';
import { handleAEMProxyRequest } from '../routes/aem-proxy.js';
import { getCookie } from '../routes/cookie.js';
import { daSourceGet } from '../routes/da-admin.js';
import { UpstreamError } from '../utils/upstream.js';

export default async function getHandler({ req, env, daCtx }) {
const { path } = daCtx;
Expand All @@ -21,11 +22,17 @@ export default async function getHandler({ req, env, daCtx }) {
if (path.startsWith('/favicon.ico')) return get404();
if (path.startsWith('/robots.txt')) return getRobots();

if (path.startsWith('/gimme_cookie')) return getCookie({ req, daCtx });
if (path.startsWith('/gimme_cookie')) return getCookie({ req, env, daCtx });

const resourceRegex = /\.(css|js|js\.map|json|xml|woff|woff2|otf|ttf|plain\.html|html)$/i;
if (resourceRegex.test(path)) {
return handleAEMProxyRequest({ req, env, daCtx });
try {
return await handleAEMProxyRequest({ req, env, daCtx });
} catch (e) {
if (!(e instanceof UpstreamError)) throw e;
console.warn(`503 GET ${path}, ${e.message}`);
return empty503(e.message);
}
}

const assetRegex = /\.(png|jpg|jpeg|webp|gif|svg|ico|avif)$/i;
Expand All @@ -35,8 +42,14 @@ export default async function getHandler({ req, env, daCtx }) {
handleAEMProxyRequest({ req, env, daCtx }),
]);

const storeRes = daSourceGetRes.status === 'fulfilled' ? daSourceGetRes.value : undefined;
const aemRes = aemProxyRes.status === 'fulfilled' ? aemProxyRes.value : undefined;
// logs a rejection rather than rethrowing it, since the other read may still answer
const settled = (result, read) => {
if (result.status === 'fulfilled') return result.value;
console.error(`${read} threw on ${path}`, result.reason);
return undefined;
};
const storeRes = settled(daSourceGetRes, 'the store read');
const aemRes = settled(aemProxyRes, 'the aem proxy');

let response;
if (storeRes?.status === 200) {
Expand Down
11 changes: 9 additions & 2 deletions src/handlers/head.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
* governing permissions and limitations under the License.
*/

import { getRobots, head404 } from '../responses/index.js';
import { empty503, getRobots, head404 } from '../responses/index.js';
import { handleAEMProxyRequest } from '../routes/aem-proxy.js';
import { daSourceHead } from '../routes/da-admin.js';
import { UpstreamError } from '../utils/upstream.js';

// for AEM we reuse the handleAEMProxyRequest for now as GETs are cheap here
// TODO refine and review for later for a full HEAD requests on AEM
Expand All @@ -31,7 +32,13 @@ export default async function headHandler({ req, env, daCtx }) {

const resourceRegex = /\.(css|js|js\.map|json|xml|woff|woff2|otf|ttf|plain\.html|html)$/i;
if (resourceRegex.test(path)) {
return aemHead({ req, env, daCtx });
try {
return await aemHead({ req, env, daCtx });
} catch (e) {
if (!(e instanceof UpstreamError)) throw e;
console.warn(`503 HEAD ${path}, ${e.message}`);
return empty503(e.message);
}
}

const assetRegex = /\.(png|jpg|jpeg|webp|gif|svg|ico|avif)$/i;
Expand Down
6 changes: 4 additions & 2 deletions src/render/compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import rewriteIcons from './rewrite-icons.js';
import { makeImagesRelative } from './rewrite-images.js';
import extractSectionMetadata from './section-metadata.js';
import { DEFAULT_HTML_TEMPLATE } from '../utils/constants.js';
import { PREVIEW_HOST, withUpstream } from '../utils/upstream.js';

/**
* Injects AEM HTML head entries into the head node of an HTML document.
Expand Down Expand Up @@ -98,8 +99,9 @@ export async function composeHtml(daCtx, aemCtx, bodyHtmlStr, headHtmlStr) {
const bodyTree = fromHtml(bodyHtmlStr, { fragment: true });
bodyNode.children = bodyTree.children;

// fetch bulk metadata, extract metadata block from the body and merge them
const bulkMetadata = await fetchBulkMetadata(aemCtx);
// fetch bulk metadata, extract metadata block from the body and merge them. the sheet is the
// only thing here that leaves the worker, so it is the only step that names an upstream
const bulkMetadata = await withUpstream(PREVIEW_HOST, () => fetchBulkMetadata(aemCtx));
const localMetaData = extractLocalMetadata(bodyTree);
const mergedMetaData = {
...localMetaData,
Expand Down
91 changes: 91 additions & 0 deletions src/render/csp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { select } from 'hast-util-select';
import { visit } from 'unist-util-visit';

const NONCE_AEM = "'nonce-aem'";
const TRUSTED_TYPES_REQUIRE = 'require-trusted-types-for';

function directiveHasNonce(content, name) {
return content
.split(';')
.some((directive) => {
const [directiveName, ...values] = directive.trim().split(/\s+/);
return directiveName === name && values.includes(NONCE_AEM);
});
}

function removeTrustedTypesRequire(content) {
const directives = content.split(';');
const filtered = directives.filter(
(directive) => directive.trim().split(/\s+/, 1)[0].toLowerCase() !== TRUSTED_TYPES_REQUIRE,
);
if (filtered.length !== directives.length) {
console.warn(`Removed ${TRUSTED_TYPES_REQUIRE} from the composed CSP meta.`);
}
return filtered.join(';');
}

function createNonce() {
const array = new Uint8Array(18);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array));
}

/**
* @param {import('hast').Root} documentTree the composed document, mutated in place
* @returns {string|undefined} the nonce to stamp on injected scripts, or undefined when the
* page carries no policy that asks for one
*/
export default function applyCsp(documentTree) {
// head.html owns the policy and placeholders; the body is author content that round-trips
// rewriting here is what lets the config service answer head.html for UE too. reading it from
// the preview host for the pipeline's own rewrite was the alternative, at a round trip per page
const scope = select('head', documentTree) ?? documentTree;
const meta = select('meta[http-equiv="content-security-policy" i]', scope);
const content = meta?.properties.content;
if (typeof content !== 'string' || !content.includes(NONCE_AEM)) {
return undefined;
}

const scriptNonce = directiveHasNonce(content, 'script-src');
const styleNonce = directiveHasNonce(content, 'style-src');
const nonce = createNonce();

meta.properties.content = removeTrustedTypesRequire(
content.replaceAll(NONCE_AEM, `'nonce-${nonce}'`),
);
// the pipeline moves this policy to a response header; we keep it in the document because
// frame-ancestors sent as a header stops the editor framing the page, and a meta ignores it
delete meta.properties['move-to-http-header'];
delete meta.properties['move-as-header'];

visit(scope, (node) => {
if (node.properties?.nonce !== 'aem') return;

if (scriptNonce
&& (node.tagName === 'script'
|| (node.tagName === 'link' && node.properties.as === 'script'))) {
node.properties.nonce = nonce;
return;
}

if (styleNonce
&& (node.tagName === 'style'
|| (node.tagName === 'link' && node.properties.rel?.includes('stylesheet')))) {
node.properties.nonce = nonce;
}
});

return scriptNonce ? nonce : undefined;
}
6 changes: 5 additions & 1 deletion src/render/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { select } from 'hast-util-select';
import { readBlockConfig } from '../utils/hast.js';
import { withAemAuth } from '../utils/aemCtx.js';

const TIMEOUT_MS = 5 * 1000;

export function extractLocalMetadata(bodyTree) {
const metaBlock = select('div.metadata', bodyTree);
let metaConfig = {};
Expand Down Expand Up @@ -157,7 +159,9 @@ export class Modifiers {

export async function fetchBulkMetadata(aemCtx) {
const url = new URL('/metadata.json', aemCtx.previewUrl);
const response = await fetch(url, withAemAuth(aemCtx));
const response = await fetch(url, withAemAuth(aemCtx, {
signal: AbortSignal.timeout(TIMEOUT_MS),
}));

if (response.ok) {
const json = await response.json();
Expand Down
6 changes: 5 additions & 1 deletion src/responses/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export function post503(message = '', error = '') {
});
}

export function post404(message = '') {
return daResp({ body: message, status: 404, contentType: 'text/plain; charset=utf-8' });
}

// RFC 9110 requires an Allow header on a 405, and reads are what is left once the write is gone.
export function post405(message = '') {
return daResp({
Expand All @@ -91,7 +95,7 @@ export function head401() {
return new Response(null, { status: 401 });
}

export function head503(error = '') {
export function empty503(error = '') {
return new Response(null, { status: 503, headers: retryHeaders(error) });
}

Expand Down
3 changes: 2 additions & 1 deletion src/routes/aem-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* governing permissions and limitations under the License.
*/
import { getAemCtx } from '../utils/aemCtx.js';
import { PREVIEW_HOST, withUpstream } from '../utils/upstream.js';
import {
applyQuickEditToScript,
getQuickEditCookiePath,
Expand Down Expand Up @@ -47,7 +48,7 @@ export async function handleAEMProxyRequest({ req, env, daCtx }) {
}

console.log(`-> ${aemUrl.toString()}`);
let response = await fetch(req, { cf: { cacheTtl: 0 } });
let response = await withUpstream(PREVIEW_HOST, () => fetch(req, { cf: { cacheTtl: 0 } }));
console.log(`<- ${aemUrl.toString()}. ${response.status} ${response.statusText}`, { status: response.status, statusText: response.statusText });

const contentType = (response.headers.get('Content-Type') || '').toLowerCase();
Expand Down
Loading