Skip to content

Commit 1964ef9

Browse files
authored
Merge pull request #76 from adobe-rnd/feat-redirect-support
feat: honour redirects from edge
2 parents 3d3a600 + a21fcd8 commit 1964ef9

4 files changed

Lines changed: 152 additions & 4 deletions

File tree

src/global.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ declare global {
4444

4545
export interface PipelineState extends ImportedPipelineState {
4646
type: 'json' | 'html' | 'media' | 'index' | 'merchant-feed' | 'sitemap';
47+
redirect?: {
48+
status: number;
49+
location: string;
50+
};
4751
config: PipelineSiteConfig;
4852
info: PathInfo;
4953
content: PipelineContent;

src/product-html-pipe.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,17 @@ export async function productHTMLPipe(state, req) {
5555
await initConfig(state, req, res);
5656

5757
state.timer?.update('content-fetch');
58-
await fetchProductBusContent(state, req, res);
58+
await Promise.all([
59+
fetchProductBusContent(state, req, res),
60+
fetchEdgeContent(state, req, res),
61+
]);
62+
63+
if (state.redirect) {
64+
res.status = state.redirect.status;
65+
res.headers.set('location', state.redirect.location);
66+
return res;
67+
}
68+
5969
if (res.status === 404) {
6070
await fetch404(state, req, res);
6171
}
@@ -75,8 +85,6 @@ export async function productHTMLPipe(state, req) {
7585
transformImages(state);
7686
state.timer?.update('render');
7787
await html(state);
78-
79-
await fetchEdgeContent(state, req, res);
8088
await extractAuthoredMetadata(state);
8189

8290
await renderHead(state);

src/steps/fetch-edge-product.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,20 @@ export default async function fetchEdgeContent(state, req, res) {
3434
if (authorization) {
3535
headers.authorization = authorization;
3636
}
37-
const contentRes = await fetch(contentUrl, { headers });
37+
const contentRes = await fetch(contentUrl, { headers, redirect: 'manual' });
3838
if (contentRes.status === 401) {
3939
throw new PipelineStatusError(401, 'unauthorized');
4040
}
41+
42+
// Handle redirects
43+
if (contentRes.status === 301) {
44+
const location = contentRes.headers.get('location');
45+
if (location) {
46+
state.redirect = { status: 301, location };
47+
}
48+
return;
49+
}
50+
4151
if (contentRes.status === 200) {
4252
state.content.edge = await contentRes.text();
4353

test/product-html-pipe.test.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,132 @@ describe('Product HTML Pipe Test', () => {
911911
fetchMock.unmockGlobal();
912912
});
913913

914+
it('returns 301 when edge content returns a redirect', async () => {
915+
fetchMock.unmockGlobal();
916+
fetchMock.removeRoutes();
917+
const fetchMockGlobal = fetchMock.mockGlobal();
918+
919+
fetchMockGlobal.get('https://main--site--org.aem.live/products/old-product-url', {
920+
status: 301,
921+
headers: { location: '/products/new-product-url' },
922+
});
923+
924+
const s3Loader = new FileS3Loader();
925+
const state = DEFAULT_STATE(DEFAULT_CONFIG, {
926+
log: console,
927+
s3Loader,
928+
ref: 'main',
929+
path: '/products/old-product-url',
930+
partition: 'live',
931+
timer: { update: () => {} },
932+
});
933+
state.info = getPathInfo('/products/old-product-url');
934+
935+
const resp = await productHTMLPipe(
936+
state,
937+
new PipelineRequest(new URL('https://acme.com/products/old-product-url')),
938+
);
939+
940+
assert.strictEqual(resp.status, 301);
941+
assert.strictEqual(resp.headers.get('location'), '/products/new-product-url');
942+
assert.strictEqual(resp.body, '');
943+
fetchMock.unmockGlobal();
944+
});
945+
946+
it('returns 301 redirect even when product data also exists in R2', async () => {
947+
fetchMock.unmockGlobal();
948+
fetchMock.removeRoutes();
949+
const fetchMockGlobal = fetchMock.mockGlobal();
950+
951+
fetchMockGlobal.get('https://main--site--org.aem.live/products/product-simple', {
952+
status: 301,
953+
headers: { location: '/products/new-url' },
954+
});
955+
956+
const s3Loader = new FileS3Loader();
957+
const state = DEFAULT_STATE(DEFAULT_CONFIG, {
958+
log: console,
959+
s3Loader,
960+
ref: 'main',
961+
path: '/products/product-simple',
962+
partition: 'live',
963+
timer: { update: () => {} },
964+
});
965+
state.info = getPathInfo('/products/product-simple');
966+
967+
const resp = await productHTMLPipe(
968+
state,
969+
new PipelineRequest(new URL('https://acme.com/products/product-simple')),
970+
);
971+
972+
assert.strictEqual(resp.status, 301);
973+
assert.strictEqual(resp.headers.get('location'), '/products/new-url');
974+
fetchMock.unmockGlobal();
975+
});
976+
977+
it('returns 301 redirect when product is also a 404 in R2', async () => {
978+
fetchMock.unmockGlobal();
979+
fetchMock.removeRoutes();
980+
const fetchMockGlobal = fetchMock.mockGlobal();
981+
982+
fetchMockGlobal.get('https://main--site--org.aem.live/products/product-404', {
983+
status: 301,
984+
headers: { location: '/products/replacement' },
985+
});
986+
987+
const s3Loader = new FileS3Loader();
988+
const state = DEFAULT_STATE(DEFAULT_CONFIG, {
989+
log: console,
990+
s3Loader,
991+
ref: 'main',
992+
path: '/products/product-404',
993+
partition: 'live',
994+
timer: { update: () => {} },
995+
});
996+
state.info = getPathInfo('/products/product-404');
997+
998+
const resp = await productHTMLPipe(
999+
state,
1000+
new PipelineRequest(new URL('https://acme.com/products/product-404')),
1001+
);
1002+
1003+
assert.strictEqual(resp.status, 301);
1004+
assert.strictEqual(resp.headers.get('location'), '/products/replacement');
1005+
fetchMock.unmockGlobal();
1006+
});
1007+
1008+
it('ignores 301 from edge content when location header is missing', async () => {
1009+
fetchMock.unmockGlobal();
1010+
fetchMock.removeRoutes();
1011+
const fetchMockGlobal = fetchMock.mockGlobal();
1012+
1013+
fetchMockGlobal.get('https://main--site--org.aem.live/products/product-simple', {
1014+
status: 301,
1015+
headers: {},
1016+
});
1017+
1018+
const s3Loader = new FileS3Loader();
1019+
const state = DEFAULT_STATE(DEFAULT_CONFIG, {
1020+
log: console,
1021+
s3Loader,
1022+
ref: 'main',
1023+
path: '/products/product-simple',
1024+
partition: 'live',
1025+
timer: { update: () => {} },
1026+
});
1027+
state.info = getPathInfo('/products/product-simple');
1028+
1029+
const resp = await productHTMLPipe(
1030+
state,
1031+
new PipelineRequest(new URL('https://acme.com/products/product-simple')),
1032+
);
1033+
1034+
// No location → redirect is ignored, product renders normally
1035+
assert.strictEqual(resp.status, 200);
1036+
assert.ok(resp.body.includes('<h1 id="blitzmax-5000">BlitzMax 5000</h1>'));
1037+
fetchMock.unmockGlobal();
1038+
});
1039+
9141040
it('handles authored content with meta tags without name value', async () => {
9151041
// Clear any existing mocks and set up fresh
9161042
fetchMock.unmockGlobal();

0 commit comments

Comments
 (0)