@dylandepass/helix-product-pipeline 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/steps/set-cache-headers.js +5 -86
- package/src/utils/cache-headers.js +76 -0
- package/src/product-media-pipe.js +0 -64
- package/src/steps/fetch-media.js +0 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [2.2.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.1.0...v2.2.0) (2026-02-04)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Remove media pipe and move to helix-product-pipeline-worker ([645d2f5](https://github.com/adobe-rnd/helix-product-pipeline/commit/645d2f5850f0b4f5abfe25e6330c68e31508d812))
|
|
7
|
+
|
|
1
8
|
# [2.1.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.0.2...v2.1.0) (2026-02-03)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -15,4 +15,4 @@ export { productHTMLPipe } from './product-html-pipe.js';
|
|
|
15
15
|
export { productIndexPipe } from './product-index-pipe.js';
|
|
16
16
|
export { productMerchantFeedPipe } from './product-merchant-feed-pipe.js';
|
|
17
17
|
export { productSitemapPipe } from './product-sitemap-pipe.js';
|
|
18
|
-
export {
|
|
18
|
+
export { applyByoCdnHeaders } from './utils/cache-headers.js';
|
|
@@ -16,13 +16,10 @@ import {
|
|
|
16
16
|
computeProductPathKey,
|
|
17
17
|
computeSiteKey,
|
|
18
18
|
compute404Key,
|
|
19
|
-
computeMediaKeys,
|
|
20
19
|
computeAuthoredContentKey,
|
|
21
20
|
computeProductKeys,
|
|
22
21
|
} from '@dylandepass/helix-product-shared';
|
|
23
|
-
|
|
24
|
-
export const isMediaRequest = (url) => /\/media_[0-9a-f]{40,}[/a-zA-Z0-9_-]*\.[0-9a-z]+$/.test(url.pathname);
|
|
25
|
-
const BYO_CDN_TYPES = ['akamai', 'cloudflare', 'fastly', 'cloudfront'];
|
|
22
|
+
import { applyByoCdnHeaders } from '../utils/cache-headers.js';
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
25
|
* Sets the cache headers for a product
|
|
@@ -32,92 +29,29 @@ const BYO_CDN_TYPES = ['akamai', 'cloudflare', 'fastly', 'cloudfront'];
|
|
|
32
29
|
* @returns {void}
|
|
33
30
|
*/
|
|
34
31
|
export function setCachingHeaders(req, resp, cacheKeys) {
|
|
35
|
-
const url = new URL(req.url);
|
|
36
|
-
|
|
37
|
-
// determine BYO CDN type
|
|
38
|
-
let byo_cdn_type = req.headers.get('x-byo-cdn-type');
|
|
39
|
-
const via = req.headers.get('via') || '';
|
|
40
|
-
const cdn_loop = req.headers.get('cdn-loop') || '';
|
|
41
|
-
// FIXME: cloudflare seems to strip the CDN-Loop header, i.e. the worker can't see it...
|
|
42
|
-
if (!BYO_CDN_TYPES.includes(byo_cdn_type)) {
|
|
43
|
-
// sniff downstream cdn type
|
|
44
|
-
if (/[Aa]kamai/.test(via)) {
|
|
45
|
-
byo_cdn_type = 'akamai';
|
|
46
|
-
} else if (via.includes('varnish') || cdn_loop.startsWith('Fastly')) {
|
|
47
|
-
byo_cdn_type = 'fastly';
|
|
48
|
-
} else if (cdn_loop.includes('cloudflare') || req.headers.has('cf-worker')) {
|
|
49
|
-
byo_cdn_type = 'cloudflare';
|
|
50
|
-
} else if (via.includes('CloudFront')) {
|
|
51
|
-
byo_cdn_type = 'cloudfront';
|
|
52
|
-
} else {
|
|
53
|
-
// invalid/unsupported CDN type
|
|
54
|
-
byo_cdn_type = undefined;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
32
|
// cache instructions for BYO CDN and the browser
|
|
59
33
|
const pushInvalidation = req.headers.get('x-push-invalidation') === 'enabled';
|
|
34
|
+
|
|
60
35
|
// browser ttl & browser cache-control
|
|
61
36
|
let browserTTL;
|
|
62
37
|
if ([302, 400, 401].includes(resp.status)) {
|
|
63
38
|
browserTTL = 0;
|
|
64
39
|
resp.headers.set('cache-control', 'private; no-cache');
|
|
65
40
|
} else {
|
|
66
|
-
|
|
67
|
-
browserTTL = resp.status === 200 ? 2592000 : 3600;
|
|
68
|
-
} else {
|
|
69
|
-
browserTTL = 7200;
|
|
70
|
-
}
|
|
41
|
+
browserTTL = 7200;
|
|
71
42
|
resp.headers.set('cache-control', `max-age=${browserTTL}, must-revalidate`);
|
|
72
43
|
}
|
|
73
44
|
|
|
74
45
|
// CDN ttl
|
|
75
46
|
let cdnTTL;
|
|
76
47
|
if ([400, 401].includes(resp.status)) {
|
|
77
|
-
// don't cache 400 & 401
|
|
48
|
+
// don't cache 400 & 401 responses in order to avoid potential downstream caching issues
|
|
78
49
|
cdnTTL = 0;
|
|
79
|
-
} else if (isMediaRequest(url)) {
|
|
80
|
-
cdnTTL = resp.status === 200 ? 2592000 : 3600;
|
|
81
50
|
} else {
|
|
82
51
|
cdnTTL = pushInvalidation ? 172800 : 300;
|
|
83
52
|
}
|
|
84
|
-
switch (byo_cdn_type) {
|
|
85
|
-
case 'cloudflare':
|
|
86
|
-
resp.headers.set('cdn-cache-control', `max-age=${cdnTTL}, must-revalidate`);
|
|
87
|
-
break;
|
|
88
|
-
case 'fastly':
|
|
89
|
-
resp.headers.set('surrogate-control', `max-age=${cdnTTL}, stale-while-revalidate=0`);
|
|
90
|
-
break;
|
|
91
|
-
case 'akamai':
|
|
92
|
-
resp.headers.set('edge-control', `!no-store,max-age=${cdnTTL}s,downstream-ttl=${browserTTL}s`);
|
|
93
|
-
break;
|
|
94
|
-
case 'cloudfront':
|
|
95
|
-
resp.headers.set('cache-control', `max-age=${browserTTL}, s-maxage=${cdnTTL}, must-revalidate`);
|
|
96
|
-
break;
|
|
97
|
-
default:
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
53
|
|
|
101
|
-
|
|
102
|
-
// apply cache tags: transform Fastly Surrogate-Key to Cloudflare Cache-Tag
|
|
103
|
-
if (cacheKeys.length) {
|
|
104
|
-
switch (byo_cdn_type) {
|
|
105
|
-
case 'fastly':
|
|
106
|
-
resp.headers.set('surrogate-key', cacheKeys.join(' '));
|
|
107
|
-
break;
|
|
108
|
-
case 'akamai':
|
|
109
|
-
resp.headers.set('edge-cache-tag', cacheKeys.join(' '));
|
|
110
|
-
break;
|
|
111
|
-
case 'cloudflare':
|
|
112
|
-
resp.headers.set('cache-tag', `${cacheKeys.join(',')}`);
|
|
113
|
-
break;
|
|
114
|
-
case 'cloudfront':
|
|
115
|
-
// cloudfront doesn't support cache tags/keys ...
|
|
116
|
-
break;
|
|
117
|
-
default:
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
54
|
+
applyByoCdnHeaders(resp, req, cacheKeys, browserTTL, cdnTTL);
|
|
121
55
|
}
|
|
122
56
|
|
|
123
57
|
/**
|
|
@@ -172,18 +106,3 @@ export async function setProductCacheHeaders(state, req, resp) {
|
|
|
172
106
|
const productKeys = await computeProductKeys(org, site, info.path, contentBusId);
|
|
173
107
|
setCachingHeaders(req, resp, productKeys);
|
|
174
108
|
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Sets the cache headers for a product
|
|
178
|
-
* @param {PipelineState} state
|
|
179
|
-
* @param {PipelineRequest} req
|
|
180
|
-
* @param {PipelineResponse} resp
|
|
181
|
-
* @returns {Promise<void>}
|
|
182
|
-
*/
|
|
183
|
-
export async function setMediaCacheHeaders(state, req, resp) {
|
|
184
|
-
const { org, site } = state;
|
|
185
|
-
const url = new URL(req.url);
|
|
186
|
-
const keys = await computeMediaKeys(org, site, url.pathname);
|
|
187
|
-
|
|
188
|
-
setCachingHeaders(req, resp, keys);
|
|
189
|
-
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const BYO_CDN_TYPES = ['akamai', 'cloudflare', 'fastly', 'cloudfront'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detects the BYO CDN type from the request headers.
|
|
17
|
+
*
|
|
18
|
+
* @param {Request|object} req - The request object (must have headers)
|
|
19
|
+
* @returns {string|undefined} - The detected CDN type or undefined
|
|
20
|
+
*/
|
|
21
|
+
const detectByoCdnType = (req) => {
|
|
22
|
+
const { headers } = req;
|
|
23
|
+
let byoCdnType = headers.get('x-byo-cdn-type');
|
|
24
|
+
const via = headers.get('via') || '';
|
|
25
|
+
const cdnLoop = headers.get('cdn-loop') || '';
|
|
26
|
+
|
|
27
|
+
if (!BYO_CDN_TYPES.includes(byoCdnType)) {
|
|
28
|
+
if (/[Aa]kamai/.test(via)) {
|
|
29
|
+
byoCdnType = 'akamai';
|
|
30
|
+
} else if (via.includes('varnish') || cdnLoop.startsWith('Fastly')) {
|
|
31
|
+
byoCdnType = 'fastly';
|
|
32
|
+
} else if (cdnLoop.includes('cloudflare') || headers.has('cf-worker')) {
|
|
33
|
+
byoCdnType = 'cloudflare';
|
|
34
|
+
} else if (via.includes('CloudFront')) {
|
|
35
|
+
byoCdnType = 'cloudfront';
|
|
36
|
+
} else {
|
|
37
|
+
byoCdnType = undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return BYO_CDN_TYPES.includes(byoCdnType) ? byoCdnType : undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Applies BYO CDN specific cache headers to the response.
|
|
46
|
+
*
|
|
47
|
+
* @param {Response|object} resp - The response object
|
|
48
|
+
* @param {Request|object} req - The request object
|
|
49
|
+
* @param {string[]} cacheKeys - List of cache keys/tags
|
|
50
|
+
* @param {number} browserTTL - Browser TTL in seconds
|
|
51
|
+
* @param {number} cdnTTL - CDN TTL in seconds
|
|
52
|
+
*/
|
|
53
|
+
export const applyByoCdnHeaders = (resp, req, cacheKeys, browserTTL, cdnTTL) => {
|
|
54
|
+
const byoCdnType = detectByoCdnType(req);
|
|
55
|
+
const keys = cacheKeys && cacheKeys.length ? cacheKeys : [];
|
|
56
|
+
|
|
57
|
+
switch (byoCdnType) {
|
|
58
|
+
case 'cloudflare':
|
|
59
|
+
resp.headers.set('cdn-cache-control', `max-age=${cdnTTL}, must-revalidate`);
|
|
60
|
+
if (keys.length) resp.headers.set('cache-tag', keys.join(','));
|
|
61
|
+
break;
|
|
62
|
+
case 'fastly':
|
|
63
|
+
resp.headers.set('surrogate-control', `max-age=${cdnTTL}, stale-while-revalidate=0`);
|
|
64
|
+
if (keys.length) resp.headers.set('surrogate-key', keys.join(' '));
|
|
65
|
+
break;
|
|
66
|
+
case 'akamai':
|
|
67
|
+
resp.headers.set('edge-control', `!no-store,max-age=${cdnTTL}s,downstream-ttl=${browserTTL}s`);
|
|
68
|
+
if (keys.length) resp.headers.set('edge-cache-tag', keys.join(' '));
|
|
69
|
+
break;
|
|
70
|
+
case 'cloudfront':
|
|
71
|
+
resp.headers.set('cache-control', `max-age=${browserTTL}, s-maxage=${cdnTTL}, must-revalidate`);
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
-
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
-
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
-
*
|
|
7
|
-
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
-
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
-
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
-
* governing permissions and limitations under the License.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { PipelineResponse, PipelineStatusError } from '@adobe/helix-html-pipeline';
|
|
14
|
-
import { validatePathInfo } from './utils/path.js';
|
|
15
|
-
import initConfig from './steps/init-config.js';
|
|
16
|
-
import fetchMedia from './steps/fetch-media.js';
|
|
17
|
-
import { setLastModified } from './utils/last-modified.js';
|
|
18
|
-
import { setMediaCacheHeaders } from './steps/set-cache-headers.js';
|
|
19
|
-
|
|
20
|
-
export async function productMediaPipe(state, req) {
|
|
21
|
-
state.type = 'media';
|
|
22
|
-
|
|
23
|
-
if (!validatePathInfo(state.info)) {
|
|
24
|
-
return new PipelineResponse('', {
|
|
25
|
-
status: 404,
|
|
26
|
-
headers: {
|
|
27
|
-
'x-error': 'invalid path',
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const res = new PipelineResponse('');
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
await initConfig(state, req, res);
|
|
36
|
-
|
|
37
|
-
state.timer?.update('content-fetch');
|
|
38
|
-
await fetchMedia(state, res);
|
|
39
|
-
if (res.status === 404) {
|
|
40
|
-
return new PipelineResponse('', {
|
|
41
|
-
status: 404,
|
|
42
|
-
headers: {
|
|
43
|
-
'x-error': 'not found',
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
} else if (res.error) {
|
|
47
|
-
if (res.status < 400) {
|
|
48
|
-
return res;
|
|
49
|
-
}
|
|
50
|
-
throw new PipelineStatusError(res.status, res.error);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
setLastModified(state, res);
|
|
54
|
-
|
|
55
|
-
await setMediaCacheHeaders(state, req, res);
|
|
56
|
-
|
|
57
|
-
res.headers = state.content.headers;
|
|
58
|
-
res.body = state.content.data;
|
|
59
|
-
} catch (e) {
|
|
60
|
-
res.error = e.message;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return res;
|
|
64
|
-
}
|
package/src/steps/fetch-media.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2022 Adobe. All rights reserved.
|
|
3
|
-
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
-
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
-
*
|
|
7
|
-
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
-
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
-
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
-
* governing permissions and limitations under the License.
|
|
11
|
-
*/
|
|
12
|
-
import { extractLastModified, recordLastModified } from '../utils/last-modified.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Loads the content from either the content-bus or code-bus and stores it in `state.content`
|
|
16
|
-
* @param {PipelineState} state
|
|
17
|
-
* @param {PipelineResponse} res
|
|
18
|
-
* @returns {Promise<void>}
|
|
19
|
-
*/
|
|
20
|
-
export default async function fetchMedia(state, res) {
|
|
21
|
-
const {
|
|
22
|
-
info, org, site,
|
|
23
|
-
} = state;
|
|
24
|
-
const bucketId = 'helix-product-bus';
|
|
25
|
-
|
|
26
|
-
const key = `${org}/${site}/media/${info.originalFilename}`;
|
|
27
|
-
const ret = await state.s3Loader.getObject(bucketId, key);
|
|
28
|
-
|
|
29
|
-
if (ret.status === 200) {
|
|
30
|
-
res.status = 200;
|
|
31
|
-
delete res.error;
|
|
32
|
-
state.content.data = ret.body;
|
|
33
|
-
state.content.headers = ret.headers;
|
|
34
|
-
|
|
35
|
-
recordLastModified(state, res, 'content', extractLastModified(ret.headers));
|
|
36
|
-
} else {
|
|
37
|
-
// keep 404, but propagate others as 502
|
|
38
|
-
res.status = ret.status === 404 ? 404 : 502;
|
|
39
|
-
res.error = `failed to load ${info.resourcePath} from product-bus: ${ret.status}`;
|
|
40
|
-
}
|
|
41
|
-
}
|