@adobe/spacecat-shared-tokowaka-client 1.5.3 → 1.5.4
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/utils/custom-html-utils.js +24 -8
- package/test/utils/html-utils.test.js +279 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.5.4](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.3...@adobe/spacecat-shared-tokowaka-client-v1.5.4) (2026-01-21)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* edge preview api headers handling ([#1276](https://github.com/adobe/spacecat-shared/issues/1276)) ([e2a7ba8](https://github.com/adobe/spacecat-shared/commit/e2a7ba88df77ee7e369d39e623966b5760ac9d12))
|
|
7
|
+
|
|
1
8
|
# [@adobe/spacecat-shared-tokowaka-client-v1.5.3](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.2...@adobe/spacecat-shared-tokowaka-client-v1.5.3) (2026-01-21)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -24,8 +24,11 @@ function sleep(ms) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Makes an HTTP request with retry logic
|
|
28
|
-
*
|
|
27
|
+
* Makes an HTTP request with retry logic for both original and optimized HTML.
|
|
28
|
+
* Header validation logic (same for both):
|
|
29
|
+
* - No proxy AND no cache header: Return response immediately (success)
|
|
30
|
+
* - Proxy header present BUT no cache header: Retry until cache header found
|
|
31
|
+
* - Cache header present (regardless of proxy): Return response (success)
|
|
29
32
|
* @param {string} url - URL to fetch
|
|
30
33
|
* @param {Object} options - Fetch options
|
|
31
34
|
* @param {number} maxRetries - Maximum number of retries
|
|
@@ -48,23 +51,35 @@ async function fetchWithRetry(url, options, maxRetries, retryDelayMs, log, fetch
|
|
|
48
51
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
// Check for
|
|
54
|
+
// Check for edge optimize headers
|
|
52
55
|
const cacheHeader = response.headers.get('x-edge-optimize-cache');
|
|
56
|
+
const proxyHeader = response.headers.get('x-edge-optimize-proxy');
|
|
57
|
+
|
|
58
|
+
log.debug(`Headers - cache: ${cacheHeader || 'none'}, proxy: ${proxyHeader || 'none'}`);
|
|
59
|
+
|
|
60
|
+
// Case 1: Cache header present (regardless of proxy) -> Success
|
|
53
61
|
if (cacheHeader) {
|
|
54
62
|
log.debug(`Cache header found (x-edge-optimize-cache: ${cacheHeader}), stopping retry logic`);
|
|
55
63
|
return response;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
//
|
|
66
|
+
// Case 2: No cache header AND no proxy header -> Success (return immediately)
|
|
67
|
+
if (!proxyHeader) {
|
|
68
|
+
log.debug('No edge optimize headers found, proceeding as successful flow');
|
|
69
|
+
return response;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Case 3: Proxy header present BUT no cache header -> Retry until cache found
|
|
73
|
+
log.debug('Proxy header present without cache header, will retry...');
|
|
74
|
+
|
|
75
|
+
// If we haven't exhausted retries, continue
|
|
59
76
|
if (attempt < maxRetries + 1) {
|
|
60
|
-
log.debug(`No cache header found on attempt ${attempt}, will retry...`);
|
|
61
|
-
// Wait before retrying
|
|
62
77
|
log.debug(`Waiting ${retryDelayMs}ms before retry...`);
|
|
63
78
|
// eslint-disable-next-line no-await-in-loop
|
|
64
79
|
await sleep(retryDelayMs);
|
|
65
80
|
} else {
|
|
66
|
-
// Last attempt
|
|
67
|
-
log.error(`Max retries (${maxRetries}) exhausted
|
|
81
|
+
// Last attempt - throw error
|
|
82
|
+
log.error(`Max retries (${maxRetries}) exhausted. Proxy header present but cache header not found`);
|
|
68
83
|
throw new Error(`Cache header (x-edge-optimize-cache) not found after ${maxRetries} retries`);
|
|
69
84
|
}
|
|
70
85
|
} catch (error) {
|
|
@@ -145,6 +160,7 @@ export async function fetchHtmlWithWarmup(
|
|
|
145
160
|
'x-forwarded-host': forwardedHost,
|
|
146
161
|
'x-edge-optimize-api-key': apiKey,
|
|
147
162
|
'x-edge-optimize-url': urlPath,
|
|
163
|
+
'Accept-Encoding': 'identity', // Disable compression to avoid content-length: 0 issue
|
|
148
164
|
};
|
|
149
165
|
|
|
150
166
|
if (isOptimized) {
|
|
@@ -154,6 +154,150 @@ describe('HTML Utils', () => {
|
|
|
154
154
|
expect(actualUrl).to.equal('https://edge.example.com/page?tokowakaPreview=true');
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
it('should return immediately for optimized HTML when no headers present', async () => {
|
|
158
|
+
// Warmup succeeds
|
|
159
|
+
fetchStub.onCall(0).resolves({
|
|
160
|
+
ok: true,
|
|
161
|
+
status: 200,
|
|
162
|
+
statusText: 'OK',
|
|
163
|
+
headers: {
|
|
164
|
+
get: () => null,
|
|
165
|
+
},
|
|
166
|
+
text: async () => 'warmup',
|
|
167
|
+
});
|
|
168
|
+
// First actual call - no headers, should succeed
|
|
169
|
+
fetchStub.onCall(1).resolves({
|
|
170
|
+
ok: true,
|
|
171
|
+
status: 200,
|
|
172
|
+
statusText: 'OK',
|
|
173
|
+
headers: {
|
|
174
|
+
get: () => null,
|
|
175
|
+
},
|
|
176
|
+
text: async () => '<html>No headers</html>',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const html = await fetchHtmlWithWarmup(
|
|
180
|
+
'https://example.com/page',
|
|
181
|
+
'api-key',
|
|
182
|
+
'host',
|
|
183
|
+
'https://edge.example.com',
|
|
184
|
+
log,
|
|
185
|
+
true, // isOptimized
|
|
186
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(html).to.equal('<html>No headers</html>');
|
|
190
|
+
// Should succeed immediately (warmup + 1 attempt)
|
|
191
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should throw error for optimized HTML when proxy present but cache not found after retries', async () => {
|
|
195
|
+
// Warmup succeeds
|
|
196
|
+
fetchStub.onCall(0).resolves({
|
|
197
|
+
ok: true,
|
|
198
|
+
status: 200,
|
|
199
|
+
statusText: 'OK',
|
|
200
|
+
headers: {
|
|
201
|
+
get: () => null,
|
|
202
|
+
},
|
|
203
|
+
text: async () => 'warmup',
|
|
204
|
+
});
|
|
205
|
+
// All actual calls have proxy but no cache header
|
|
206
|
+
fetchStub.onCall(1).resolves({
|
|
207
|
+
ok: true,
|
|
208
|
+
status: 200,
|
|
209
|
+
statusText: 'OK',
|
|
210
|
+
headers: {
|
|
211
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
212
|
+
},
|
|
213
|
+
text: async () => '<html>Proxy only 1</html>',
|
|
214
|
+
});
|
|
215
|
+
fetchStub.onCall(2).resolves({
|
|
216
|
+
ok: true,
|
|
217
|
+
status: 200,
|
|
218
|
+
statusText: 'OK',
|
|
219
|
+
headers: {
|
|
220
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
221
|
+
},
|
|
222
|
+
text: async () => '<html>Proxy only 2</html>',
|
|
223
|
+
});
|
|
224
|
+
fetchStub.onCall(3).resolves({
|
|
225
|
+
ok: true,
|
|
226
|
+
status: 200,
|
|
227
|
+
statusText: 'OK',
|
|
228
|
+
headers: {
|
|
229
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
230
|
+
},
|
|
231
|
+
text: async () => '<html>Proxy only 3</html>',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await fetchHtmlWithWarmup(
|
|
236
|
+
'https://example.com/page',
|
|
237
|
+
'api-key',
|
|
238
|
+
'host',
|
|
239
|
+
'https://edge.example.com',
|
|
240
|
+
log,
|
|
241
|
+
true, // isOptimized
|
|
242
|
+
{ warmupDelayMs: 0, maxRetries: 2, retryDelayMs: 0 },
|
|
243
|
+
);
|
|
244
|
+
expect.fail('Should have thrown error');
|
|
245
|
+
} catch (error) {
|
|
246
|
+
expect(error.message).to.include('Failed to fetch optimized HTML');
|
|
247
|
+
expect(error.message).to.include('Cache header (x-edge-optimize-cache) not found after 2 retries');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Should have tried 3 times (initial + 2 retries) plus warmup
|
|
251
|
+
expect(fetchStub.callCount).to.equal(4);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should retry for optimized HTML when proxy present until cache found', async () => {
|
|
255
|
+
// Warmup succeeds
|
|
256
|
+
fetchStub.onCall(0).resolves({
|
|
257
|
+
ok: true,
|
|
258
|
+
status: 200,
|
|
259
|
+
statusText: 'OK',
|
|
260
|
+
headers: {
|
|
261
|
+
get: () => null,
|
|
262
|
+
},
|
|
263
|
+
text: async () => 'warmup',
|
|
264
|
+
});
|
|
265
|
+
// First call has only proxy header - should retry
|
|
266
|
+
fetchStub.onCall(1).resolves({
|
|
267
|
+
ok: true,
|
|
268
|
+
status: 200,
|
|
269
|
+
statusText: 'OK',
|
|
270
|
+
headers: {
|
|
271
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
272
|
+
},
|
|
273
|
+
text: async () => '<html>Proxy only</html>',
|
|
274
|
+
});
|
|
275
|
+
// Second call has cache header (proxy might still be there) - should succeed
|
|
276
|
+
fetchStub.onCall(2).resolves({
|
|
277
|
+
ok: true,
|
|
278
|
+
status: 200,
|
|
279
|
+
statusText: 'OK',
|
|
280
|
+
headers: {
|
|
281
|
+
get: (name) => (name === 'x-edge-optimize-cache' ? 'HIT' : null),
|
|
282
|
+
},
|
|
283
|
+
text: async () => '<html>Cached HTML</html>',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const html = await fetchHtmlWithWarmup(
|
|
287
|
+
'https://example.com/page',
|
|
288
|
+
'api-key',
|
|
289
|
+
'host',
|
|
290
|
+
'https://edge.example.com',
|
|
291
|
+
log,
|
|
292
|
+
true, // isOptimized
|
|
293
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(html).to.equal('<html>Cached HTML</html>');
|
|
297
|
+
// Should retry when only proxy present (warmup + 2 attempts)
|
|
298
|
+
expect(fetchStub.callCount).to.equal(3);
|
|
299
|
+
});
|
|
300
|
+
|
|
157
301
|
it('should throw error when HTTP response is not ok', async () => {
|
|
158
302
|
// Warmup succeeds
|
|
159
303
|
fetchStub.onCall(0).resolves({
|
|
@@ -285,7 +429,7 @@ describe('HTML Utils', () => {
|
|
|
285
429
|
}
|
|
286
430
|
});
|
|
287
431
|
|
|
288
|
-
it('should
|
|
432
|
+
it('should return immediately when no edge optimize headers are present', async () => {
|
|
289
433
|
// Warmup succeeds
|
|
290
434
|
fetchStub.onCall(0).resolves({
|
|
291
435
|
ok: true,
|
|
@@ -296,7 +440,7 @@ describe('HTML Utils', () => {
|
|
|
296
440
|
},
|
|
297
441
|
text: async () => 'warmup',
|
|
298
442
|
});
|
|
299
|
-
// First actual call - no
|
|
443
|
+
// First actual call - no headers, should succeed immediately
|
|
300
444
|
fetchStub.onCall(1).resolves({
|
|
301
445
|
ok: true,
|
|
302
446
|
status: 200,
|
|
@@ -304,17 +448,58 @@ describe('HTML Utils', () => {
|
|
|
304
448
|
headers: {
|
|
305
449
|
get: () => null,
|
|
306
450
|
},
|
|
307
|
-
text: async () => '<html>No
|
|
451
|
+
text: async () => '<html>No headers</html>',
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const html = await fetchHtmlWithWarmup(
|
|
455
|
+
'https://example.com/page',
|
|
456
|
+
'api-key',
|
|
457
|
+
'host',
|
|
458
|
+
'https://edge.example.com',
|
|
459
|
+
log,
|
|
460
|
+
false,
|
|
461
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(html).to.equal('<html>No headers</html>');
|
|
465
|
+
// Should succeed immediately without retry (warmup + 1 attempt)
|
|
466
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should retry when proxy header present without cache until cache is found', async () => {
|
|
470
|
+
// Warmup succeeds
|
|
471
|
+
fetchStub.onCall(0).resolves({
|
|
472
|
+
ok: true,
|
|
473
|
+
status: 200,
|
|
474
|
+
statusText: 'OK',
|
|
475
|
+
headers: {
|
|
476
|
+
get: () => null,
|
|
477
|
+
},
|
|
478
|
+
text: async () => 'warmup',
|
|
479
|
+
});
|
|
480
|
+
// First call has proxy header but no cache - should retry
|
|
481
|
+
fetchStub.onCall(1).resolves({
|
|
482
|
+
ok: true,
|
|
483
|
+
status: 200,
|
|
484
|
+
statusText: 'OK',
|
|
485
|
+
headers: {
|
|
486
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
487
|
+
},
|
|
488
|
+
text: async () => '<html>Proxy only</html>',
|
|
308
489
|
});
|
|
309
|
-
// Second
|
|
490
|
+
// Second call has both headers - should succeed
|
|
310
491
|
fetchStub.onCall(2).resolves({
|
|
311
492
|
ok: true,
|
|
312
493
|
status: 200,
|
|
313
494
|
statusText: 'OK',
|
|
314
495
|
headers: {
|
|
315
|
-
get: (name) =>
|
|
496
|
+
get: (name) => {
|
|
497
|
+
if (name === 'x-edge-optimize-cache') return 'HIT';
|
|
498
|
+
if (name === 'x-edge-optimize-proxy') return 'true';
|
|
499
|
+
return null;
|
|
500
|
+
},
|
|
316
501
|
},
|
|
317
|
-
text: async () => '<html>
|
|
502
|
+
text: async () => '<html>Both headers</html>',
|
|
318
503
|
});
|
|
319
504
|
|
|
320
505
|
const html = await fetchHtmlWithWarmup(
|
|
@@ -327,12 +512,12 @@ describe('HTML Utils', () => {
|
|
|
327
512
|
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
328
513
|
);
|
|
329
514
|
|
|
330
|
-
expect(html).to.equal('<html>
|
|
331
|
-
// Should
|
|
515
|
+
expect(html).to.equal('<html>Both headers</html>');
|
|
516
|
+
// Should retry when only proxy present (warmup + 2 attempts)
|
|
332
517
|
expect(fetchStub.callCount).to.equal(3);
|
|
333
518
|
});
|
|
334
519
|
|
|
335
|
-
it('should throw error when
|
|
520
|
+
it('should throw error when proxy header present but cache not found after max retries', async () => {
|
|
336
521
|
// Warmup succeeds
|
|
337
522
|
fetchStub.onCall(0).resolves({
|
|
338
523
|
ok: true,
|
|
@@ -343,33 +528,33 @@ describe('HTML Utils', () => {
|
|
|
343
528
|
},
|
|
344
529
|
text: async () => 'warmup',
|
|
345
530
|
});
|
|
346
|
-
// All actual calls
|
|
531
|
+
// All actual calls have proxy but no cache header
|
|
347
532
|
fetchStub.onCall(1).resolves({
|
|
348
533
|
ok: true,
|
|
349
534
|
status: 200,
|
|
350
535
|
statusText: 'OK',
|
|
351
536
|
headers: {
|
|
352
|
-
get: () => null,
|
|
537
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
353
538
|
},
|
|
354
|
-
text: async () => '<html>
|
|
539
|
+
text: async () => '<html>Proxy only 1</html>',
|
|
355
540
|
});
|
|
356
541
|
fetchStub.onCall(2).resolves({
|
|
357
542
|
ok: true,
|
|
358
543
|
status: 200,
|
|
359
544
|
statusText: 'OK',
|
|
360
545
|
headers: {
|
|
361
|
-
get: () => null,
|
|
546
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
362
547
|
},
|
|
363
|
-
text: async () => '<html>
|
|
548
|
+
text: async () => '<html>Proxy only 2</html>',
|
|
364
549
|
});
|
|
365
550
|
fetchStub.onCall(3).resolves({
|
|
366
551
|
ok: true,
|
|
367
552
|
status: 200,
|
|
368
553
|
statusText: 'OK',
|
|
369
554
|
headers: {
|
|
370
|
-
get: () => null,
|
|
555
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
371
556
|
},
|
|
372
|
-
text: async () => '<html>
|
|
557
|
+
text: async () => '<html>Proxy only 3</html>',
|
|
373
558
|
});
|
|
374
559
|
|
|
375
560
|
try {
|
|
@@ -428,6 +613,84 @@ describe('HTML Utils', () => {
|
|
|
428
613
|
// Should not retry if cache header found on first attempt
|
|
429
614
|
expect(fetchStub.callCount).to.equal(2); // warmup + 1 actual
|
|
430
615
|
});
|
|
616
|
+
|
|
617
|
+
it('should return immediately when cache header is present (with or without proxy)', async () => {
|
|
618
|
+
// Warmup succeeds
|
|
619
|
+
fetchStub.onCall(0).resolves({
|
|
620
|
+
ok: true,
|
|
621
|
+
status: 200,
|
|
622
|
+
statusText: 'OK',
|
|
623
|
+
headers: {
|
|
624
|
+
get: () => null,
|
|
625
|
+
},
|
|
626
|
+
text: async () => 'warmup',
|
|
627
|
+
});
|
|
628
|
+
// First actual call has cache header (proxy may or may not be present)
|
|
629
|
+
fetchStub.onCall(1).resolves({
|
|
630
|
+
ok: true,
|
|
631
|
+
status: 200,
|
|
632
|
+
statusText: 'OK',
|
|
633
|
+
headers: {
|
|
634
|
+
get: (name) => {
|
|
635
|
+
if (name === 'x-edge-optimize-cache') return 'HIT';
|
|
636
|
+
if (name === 'x-edge-optimize-proxy') return 'true';
|
|
637
|
+
return null;
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
text: async () => '<html>Cache header present</html>',
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const html = await fetchHtmlWithWarmup(
|
|
644
|
+
'https://example.com/page',
|
|
645
|
+
'api-key',
|
|
646
|
+
'host',
|
|
647
|
+
'https://edge.example.com',
|
|
648
|
+
log,
|
|
649
|
+
false,
|
|
650
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
expect(html).to.equal('<html>Cache header present</html>');
|
|
654
|
+
// Should succeed immediately when cache header present (warmup + 1 attempt)
|
|
655
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('should succeed when only cache header is present (no proxy header)', async () => {
|
|
659
|
+
// Warmup succeeds
|
|
660
|
+
fetchStub.onCall(0).resolves({
|
|
661
|
+
ok: true,
|
|
662
|
+
status: 200,
|
|
663
|
+
statusText: 'OK',
|
|
664
|
+
headers: {
|
|
665
|
+
get: () => null,
|
|
666
|
+
},
|
|
667
|
+
text: async () => 'warmup',
|
|
668
|
+
});
|
|
669
|
+
// First actual call has only cache header
|
|
670
|
+
fetchStub.onCall(1).resolves({
|
|
671
|
+
ok: true,
|
|
672
|
+
status: 200,
|
|
673
|
+
statusText: 'OK',
|
|
674
|
+
headers: {
|
|
675
|
+
get: (name) => (name === 'x-edge-optimize-cache' ? 'HIT' : null),
|
|
676
|
+
},
|
|
677
|
+
text: async () => '<html>Cache only HTML</html>',
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const html = await fetchHtmlWithWarmup(
|
|
681
|
+
'https://example.com/page',
|
|
682
|
+
'api-key',
|
|
683
|
+
'host',
|
|
684
|
+
'https://edge.example.com',
|
|
685
|
+
log,
|
|
686
|
+
false,
|
|
687
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
expect(html).to.equal('<html>Cache only HTML</html>');
|
|
691
|
+
// Should succeed immediately with cache header only (warmup + 1 attempt)
|
|
692
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
693
|
+
});
|
|
431
694
|
});
|
|
432
695
|
|
|
433
696
|
describe('calculateForwardedHost', () => {
|