@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.6 → 3.9.0-ultramodern.8

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.
@@ -5,9 +5,6 @@ import {
5
5
  BFF_ENVELOPE_HEADER,
6
6
  BFF_OPERATION_CONTEXT_DETAIL_HEADER,
7
7
  BFF_OPERATION_CONTEXT_HEADER,
8
- type IdentityBindingOptions,
9
- type OperationContext,
10
- type OperationContractOptions,
11
8
  type TransportTarget,
12
9
  } from '../../src/request-policy/types';
13
10
 
@@ -23,37 +20,6 @@ const OPERATION_TRACEPARENT =
23
20
  '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01';
24
21
 
25
22
  type HeaderMap = Record<string, any>;
26
- type Method = 'GET' | 'POST';
27
-
28
- type OutboundScenario = {
29
- name: string;
30
- target: TransportTarget;
31
- method: Method;
32
- requestId?: string;
33
- domain?: string;
34
- incomingHeaders?: HeaderMap;
35
- payload: {
36
- body?: string;
37
- headers?: HeaderMap;
38
- };
39
- identityBinding?: IdentityBindingOptions;
40
- requireEnvelope?: boolean;
41
- allowCrossOriginEnvelope?: boolean;
42
- operationContract?: OperationContractOptions;
43
- operationContext?: OperationContext;
44
- expectedUrl: string;
45
- expectedBody?: string;
46
- expectedStaticHeaders: HeaderMap;
47
- assertDynamicHeaders?: (headers: HeaderMap) => void;
48
- };
49
-
50
- const restoreStrictDefaultRequestId = (value: string | undefined) => {
51
- if (typeof value === 'undefined') {
52
- delete process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID;
53
- return;
54
- }
55
- process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID = value;
56
- };
57
23
 
58
24
  const createHarness = (
59
25
  target: TransportTarget,
@@ -68,254 +34,118 @@ const createHarness = (
68
34
  const requestFactory =
69
35
  target === 'server' ? createServerClient() : createBrowserClient();
70
36
 
71
- return {
72
- request,
73
- requestFactory,
74
- };
37
+ return { request, requestFactory };
75
38
  };
76
39
 
77
- const assertRequestInit = (scenario: OutboundScenario, init: RequestInit) => {
78
- const headers = init.headers as HeaderMap;
40
+ describe('requestFactory outbound request contract', () => {
41
+ test('browser GET drops the caller body, canonicalizes accept and withholds the incoming traceparent', async () => {
42
+ const { request, requestFactory } = createHarness('browser', {
43
+ traceparent: INCOMING_TRACEPARENT,
44
+ });
45
+ requestFactory.configure({ request: request as unknown as typeof fetch });
79
46
 
80
- expect(init.method).toBe(scenario.method);
81
- expect(init.body).toBe(scenario.expectedBody);
82
- for (const [key, value] of Object.entries(scenario.expectedStaticHeaders)) {
83
- expect(headers[key]).toBe(value);
84
- }
85
- expect(
86
- Object.keys(headers).filter(key => key.toLowerCase() === 'accept'),
87
- ).toEqual(['accept']);
47
+ const send = requestFactory.createRequest({
48
+ path: REQUEST_PATH,
49
+ method: 'GET',
50
+ port: PORT,
51
+ });
52
+ await send({
53
+ body: 'drop-me',
54
+ headers: { Accept: 'application/problem+json' },
55
+ });
88
56
 
89
- scenario.assertDynamicHeaders?.(headers);
90
- };
57
+ const [url, init] = request.mock.calls[0];
58
+ expect(String(url)).toBe(REQUEST_PATH);
59
+ expect(init.method).toBe('GET');
60
+ expect(init.body).toBeUndefined();
61
+ const headers = init.headers as HeaderMap;
62
+ expect(headers.accept).toBe(ACCEPT_HEADER);
63
+ expect(
64
+ Object.keys(headers).filter(key => key.toLowerCase() === 'accept'),
65
+ ).toEqual(['accept']);
66
+ expect(headers.traceparent).toBeUndefined();
67
+ });
91
68
 
92
- const outboundScenarios: OutboundScenario[] = [
93
- {
94
- name: 'browser GET strips caller body and canonicalizes accept',
95
- target: 'browser',
96
- method: 'GET',
97
- payload: {
98
- body: 'drop-me',
99
- headers: {
100
- Accept: 'application/problem+json',
101
- },
102
- },
103
- expectedUrl: REQUEST_PATH,
104
- expectedStaticHeaders: {
105
- 'Content-Type': 'text/plain',
106
- accept: ACCEPT_HEADER,
107
- },
108
- },
109
- {
110
- name: 'browser POST keeps caller body without propagating incoming traceparent',
111
- target: 'browser',
112
- method: 'POST',
113
- incomingHeaders: {
114
- traceparent: INCOMING_TRACEPARENT,
115
- },
116
- payload: {
117
- body: 'keep-me',
118
- headers: {
119
- Accept: 'application/problem+json',
120
- },
121
- },
122
- expectedUrl: REQUEST_PATH,
123
- expectedBody: 'keep-me',
124
- expectedStaticHeaders: {
125
- 'Content-Type': 'text/plain',
126
- accept: ACCEPT_HEADER,
127
- },
128
- },
129
- {
130
- name: 'server GET strips caller body and propagates incoming traceparent',
131
- target: 'server',
132
- method: 'GET',
133
- incomingHeaders: {
134
- traceparent: INCOMING_TRACEPARENT,
135
- },
136
- payload: {
137
- body: 'drop-me',
138
- headers: {
139
- Accept: 'application/problem+json',
140
- },
141
- },
142
- expectedUrl: SERVER_URL,
143
- expectedStaticHeaders: {
144
- 'Content-Type': 'text/plain',
145
- accept: ACCEPT_HEADER,
146
- traceparent: INCOMING_TRACEPARENT,
147
- },
148
- },
149
- {
150
- name: 'server POST keeps caller body and propagates incoming traceparent',
151
- target: 'server',
152
- method: 'POST',
153
- incomingHeaders: {
154
- traceparent: INCOMING_TRACEPARENT,
155
- },
156
- payload: {
157
- body: 'keep-me',
158
- headers: {
159
- Accept: 'application/problem+json',
160
- },
161
- },
162
- expectedUrl: SERVER_URL,
163
- expectedBody: 'keep-me',
164
- expectedStaticHeaders: {
165
- 'Content-Type': 'text/plain',
166
- accept: ACCEPT_HEADER,
69
+ test('server POST keeps the caller body and propagates the incoming traceparent', async () => {
70
+ const { request, requestFactory } = createHarness('server', {
167
71
  traceparent: INCOMING_TRACEPARENT,
168
- },
169
- },
170
- {
171
- name: 'protected identity binding replaces caller case variant',
172
- target: 'server',
173
- method: 'POST',
174
- payload: {
175
- body: 'keep-me',
176
- headers: {
177
- Accept: 'application/problem+json',
178
- 'X-Tenant-Id': 'caller-tenant',
179
- },
180
- },
181
- identityBinding: {
182
- enabled: true,
183
- strict: false,
184
- protectedHeaders: ['x-tenant-id'],
185
- deriveHeaders: () => ({
186
- 'x-tenant-id': 'bound-tenant',
187
- }),
188
- },
189
- expectedUrl: SERVER_URL,
190
- expectedBody: 'keep-me',
191
- expectedStaticHeaders: {
192
- 'Content-Type': 'text/plain',
193
- accept: ACCEPT_HEADER,
194
- 'x-tenant-id': 'bound-tenant',
195
- },
196
- },
197
- {
198
- name: 'configured secured request emits envelope and operation context',
199
- target: 'server',
200
- method: 'POST',
201
- requestId: 'producer-checkout',
202
- domain: 'https://producer.example',
203
- incomingHeaders: {
72
+ });
73
+ requestFactory.configure({ request: request as unknown as typeof fetch });
74
+
75
+ const send = requestFactory.createRequest({
76
+ path: REQUEST_PATH,
77
+ method: 'POST',
78
+ port: PORT,
79
+ });
80
+ await send({ body: 'keep-me' });
81
+
82
+ const [url, init] = request.mock.calls[0];
83
+ expect(String(url)).toBe(SERVER_URL);
84
+ expect(init.method).toBe('POST');
85
+ expect(init.body).toBe('keep-me');
86
+ expect((init.headers as HeaderMap).traceparent).toBe(INCOMING_TRACEPARENT);
87
+ });
88
+
89
+ test('secured producer request emits the cross-origin envelope and operation context', async () => {
90
+ const requestId = 'producer-checkout';
91
+ const { request, requestFactory } = createHarness('server', {
204
92
  origin: CONSUMER_ORIGIN,
205
- },
206
- payload: {
207
- body: 'keep-me',
208
- headers: {
209
- Accept: 'application/problem+json',
93
+ });
94
+
95
+ requestFactory.configure({
96
+ request: request as unknown as typeof fetch,
97
+ requestId,
98
+ setDomain: () => 'https://producer.example',
99
+ requireEnvelope: true,
100
+ allowCrossOriginEnvelope: true,
101
+ operationContract: {
102
+ enabled: true,
103
+ requireSchemaHash: true,
104
+ requireOperationVersion: true,
210
105
  },
211
- },
212
- requireEnvelope: true,
213
- allowCrossOriginEnvelope: true,
214
- operationContract: {
215
- enabled: true,
216
- requireSchemaHash: true,
217
- requireOperationVersion: true,
218
- },
219
- operationContext: {
220
- operationId: 'create-widget',
221
- routePath: REQUEST_PATH,
222
- method: 'POST',
223
- schemaHash: 'sha256:create-widget',
224
- operationVersion: 3,
225
- traceparent: OPERATION_TRACEPARENT,
226
- },
227
- expectedUrl: PRODUCER_URL,
228
- expectedBody: 'keep-me',
229
- expectedStaticHeaders: {
230
- 'Content-Type': 'text/plain',
231
- accept: ACCEPT_HEADER,
232
- traceparent: OPERATION_TRACEPARENT,
233
- [BFF_OPERATION_CONTEXT_HEADER]: 'producer-checkout:create-widget',
234
- },
235
- assertDynamicHeaders: headers => {
236
- const envelope = JSON.parse(headers[BFF_ENVELOPE_HEADER]);
237
- expect(envelope).toMatchObject({
238
- requestId: 'producer-checkout',
239
- target: 'server',
240
- sourceOrigin: CONSUMER_ORIGIN,
241
- targetOrigin: 'https://producer.example',
242
- traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
243
- spanId: 'bbbbbbbbbbbbbbbb',
244
- });
245
- expect(typeof envelope.timestamp).toBe('number');
106
+ });
246
107
 
247
- const operationContext = JSON.parse(
248
- headers[BFF_OPERATION_CONTEXT_DETAIL_HEADER],
249
- );
250
- expect(operationContext).toMatchObject({
251
- requestId: 'producer-checkout',
252
- operationId: 'producer-checkout:create-widget',
108
+ const send = requestFactory.createRequest({
109
+ path: REQUEST_PATH,
110
+ method: 'POST',
111
+ port: PORT,
112
+ requestId,
113
+ operationContext: {
114
+ operationId: 'create-widget',
253
115
  routePath: REQUEST_PATH,
254
116
  method: 'POST',
255
117
  schemaHash: 'sha256:create-widget',
256
118
  operationVersion: 3,
257
- traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
258
- spanId: 'bbbbbbbbbbbbbbbb',
259
- });
260
- },
261
- },
262
- ];
263
-
264
- describe('requestFactory outbound request contract', () => {
265
- for (const scenario of outboundScenarios) {
266
- test(scenario.name, async () => {
267
- const strictDefaultRequestId =
268
- process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID;
269
- delete process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID;
270
-
271
- try {
272
- const { request, requestFactory } = createHarness(
273
- scenario.target,
274
- scenario.incomingHeaders,
275
- );
276
- const requestId = scenario.requestId || 'default';
277
-
278
- requestFactory.configure({
279
- request: request as unknown as typeof fetch,
280
- requestId,
281
- ...(requestId !== 'default'
282
- ? { setDomain: () => scenario.domain || '' }
283
- : {}),
284
- ...(scenario.identityBinding
285
- ? { identityBinding: scenario.identityBinding }
286
- : {}),
287
- ...(typeof scenario.requireEnvelope === 'boolean'
288
- ? { requireEnvelope: scenario.requireEnvelope }
289
- : {}),
290
- ...(typeof scenario.allowCrossOriginEnvelope === 'boolean'
291
- ? { allowCrossOriginEnvelope: scenario.allowCrossOriginEnvelope }
292
- : {}),
293
- ...(scenario.operationContract
294
- ? { operationContract: scenario.operationContract }
295
- : {}),
296
- });
297
-
298
- const send = requestFactory.createRequest({
299
- path: REQUEST_PATH,
300
- method: scenario.method,
301
- port: PORT,
302
- requestId,
303
- operationContext: scenario.operationContext,
304
- });
305
-
306
- await send(scenario.payload);
119
+ traceparent: OPERATION_TRACEPARENT,
120
+ },
121
+ });
122
+ await send({ body: 'keep-me' });
307
123
 
308
- expect(request).toHaveBeenCalledTimes(1);
309
- const [url, init] = request.mock.calls[0];
310
- expect(String(url)).toBe(scenario.expectedUrl);
311
- assertRequestInit(scenario, init as RequestInit);
312
- } finally {
313
- restoreStrictDefaultRequestId(strictDefaultRequestId);
314
- }
124
+ const [url, init] = request.mock.calls[0];
125
+ expect(String(url)).toBe(PRODUCER_URL);
126
+ const headers = init.headers as HeaderMap;
127
+ expect(headers.traceparent).toBe(OPERATION_TRACEPARENT);
128
+ expect(headers[BFF_OPERATION_CONTEXT_HEADER]).toBe(
129
+ 'producer-checkout:create-widget',
130
+ );
131
+ expect(JSON.parse(headers[BFF_ENVELOPE_HEADER])).toMatchObject({
132
+ requestId,
133
+ target: 'server',
134
+ sourceOrigin: CONSUMER_ORIGIN,
135
+ targetOrigin: 'https://producer.example',
136
+ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
137
+ spanId: 'bbbbbbbbbbbbbbbb',
315
138
  });
316
- }
139
+ expect(
140
+ JSON.parse(headers[BFF_OPERATION_CONTEXT_DETAIL_HEADER]),
141
+ ).toMatchObject({
142
+ operationId: 'producer-checkout:create-widget',
143
+ schemaHash: 'sha256:create-widget',
144
+ operationVersion: 3,
145
+ });
146
+ });
317
147
 
318
- test('inputParams forwards only allowlisted and server-derived identity headers', async () => {
148
+ test('forwards only allowlisted and server-derived identity headers', async () => {
319
149
  const requestId = 'producer-input-params-identity';
320
150
  const { request, requestFactory } = createHarness('server', {
321
151
  'x-tenant-id': 'tenant-server',
@@ -340,10 +170,8 @@ describe('requestFactory outbound request contract', () => {
340
170
  httpMethodDecider: 'inputParams',
341
171
  requestId,
342
172
  });
343
-
344
173
  await send('widget');
345
174
 
346
- expect(request).toHaveBeenCalledTimes(1);
347
175
  const headers = request.mock.calls[0][1]?.headers as HeaderMap;
348
176
  expect(headers).toMatchObject({
349
177
  'x-tenant-id': 'tenant-server',
@@ -353,94 +181,44 @@ describe('requestFactory outbound request contract', () => {
353
181
  expect(headers['x-private-incoming']).toBeUndefined();
354
182
  });
355
183
 
356
- test('uploader forwards policy, allowlisted, and server-derived headers', async () => {
184
+ test('uploader sends the file as form data under the same header policy', async () => {
357
185
  const requestId = 'producer-uploader-identity';
358
186
  const { request, requestFactory } = createHarness('server', {
359
187
  origin: CONSUMER_ORIGIN,
360
188
  'x-tenant-id': 'tenant-server',
361
- 'x-subject-id': 'subject-server',
362
- 'x-forwarded-feature': 'feature-server',
363
189
  'x-private-incoming': 'must-not-forward',
364
190
  });
365
191
 
366
192
  requestFactory.configure({
367
193
  request: request as unknown as typeof fetch,
368
194
  requestId,
369
- allowedHeaders: ['x-forwarded-feature'],
370
- operationContract: {
371
- enabled: true,
372
- requireSchemaHash: true,
373
- requireOperationVersion: true,
374
- },
375
- requireEnvelope: true,
376
- allowCrossOriginEnvelope: true,
195
+ operationContract: { enabled: false },
196
+ requireEnvelope: false,
377
197
  setDomain: () => 'https://producer.example',
378
198
  });
379
199
 
380
200
  const upload = requestFactory.createUploader({
381
201
  path: REQUEST_PATH,
382
202
  requestId,
383
- operationContext: {
384
- operationId: 'upload-widget',
385
- routePath: REQUEST_PATH,
386
- method: 'POST',
387
- schemaHash: 'sha256:upload-widget',
388
- operationVersion: 1,
389
- traceparent: OPERATION_TRACEPARENT,
390
- },
391
203
  });
392
-
393
204
  await upload({
394
205
  files: {
395
206
  file: new File(['widget'], 'widget.txt', { type: 'text/plain' }),
396
207
  },
397
208
  });
398
209
 
399
- expect(request).toHaveBeenCalledTimes(1);
400
210
  const [, init] = request.mock.calls[0];
401
211
  expect(init.method).toBe('POST');
402
212
  expect(init.body).toBeInstanceOf(FormData);
403
- const form = init.body as FormData;
404
- const uploaded = form.get('file');
405
- expect(uploaded).toBeInstanceOf(File);
406
- expect(uploaded).toMatchObject({
407
- name: 'widget.txt',
408
- type: 'text/plain',
409
- });
410
- expect(await (uploaded as File).text()).toBe('widget');
411
- const headers = request.mock.calls[0][1]?.headers as HeaderMap;
412
- expect(headers).toMatchObject({
413
- 'x-tenant-id': 'tenant-server',
414
- 'x-subject-id': 'subject-server',
415
- 'x-forwarded-feature': 'feature-server',
416
- });
213
+ const uploaded = (init.body as FormData).get('file') as File;
214
+ expect(uploaded.name).toBe('widget.txt');
215
+ expect(await uploaded.text()).toBe('widget');
216
+ const headers = init.headers as HeaderMap;
217
+ expect(headers['x-tenant-id']).toBe('tenant-server');
417
218
  expect(headers['x-private-incoming']).toBeUndefined();
418
- const envelope = JSON.parse(headers[BFF_ENVELOPE_HEADER]);
419
- expect(envelope).toMatchObject({
420
- requestId,
421
- target: 'server',
422
- sourceOrigin: CONSUMER_ORIGIN,
423
- targetOrigin: 'https://producer.example',
424
- });
425
- expect(headers[BFF_OPERATION_CONTEXT_HEADER]).toBe(
426
- `${requestId}:upload-widget`,
427
- );
428
- const operationContext = JSON.parse(
429
- headers[BFF_OPERATION_CONTEXT_DETAIL_HEADER],
430
- );
431
- expect(operationContext).toMatchObject({
432
- requestId,
433
- operationId: `${requestId}:upload-widget`,
434
- routePath: REQUEST_PATH,
435
- method: 'POST',
436
- schemaHash: 'sha256:upload-widget',
437
- operationVersion: 1,
438
- traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
439
- spanId: 'bbbbbbbbbbbbbbbb',
440
- });
441
219
  });
442
220
 
443
- test('forwards allowlisted and resolved headers without casing assumptions', async () => {
221
+ test('resolved headers replace caller and incoming values without injecting unlisted ones', async () => {
444
222
  const resolveHeaders = rs.fn(() => ({
445
223
  AUTHORIZATION: 'Bearer resolved',
446
224
  'x-injected': 'must-not-forward',
@@ -460,9 +238,7 @@ describe('requestFactory outbound request contract', () => {
460
238
  method: 'GET',
461
239
  port: PORT,
462
240
  });
463
- await send({
464
- headers: { Authorization: 'Bearer caller' },
465
- });
241
+ await send({ headers: { Authorization: 'Bearer caller' } });
466
242
 
467
243
  expect(resolveHeaders).toHaveBeenCalledWith(
468
244
  expect.objectContaining({
@@ -470,19 +246,15 @@ describe('requestFactory outbound request contract', () => {
470
246
  }),
471
247
  );
472
248
  const headers = request.mock.calls[0][1]?.headers as HeaderMap;
473
- expect(headers).toMatchObject({
474
- authorization: 'Bearer resolved',
475
- });
249
+ expect(headers.authorization).toBe('Bearer resolved');
476
250
  expect(headers['x-injected']).toBeUndefined();
477
251
  expect(
478
252
  Object.keys(headers).filter(key => key.toLowerCase() === 'authorization'),
479
253
  ).toEqual(['authorization']);
480
254
  });
481
255
 
482
- test('reconfiguration removes omitted header and envelope policy', async () => {
483
- const resolveHeaders = rs.fn(() => ({
484
- authorization: 'Bearer stale',
485
- }));
256
+ test('reconfiguration drops an omitted header and envelope policy', async () => {
257
+ const resolveHeaders = rs.fn(() => ({ authorization: 'Bearer stale' }));
486
258
  const { request, requestFactory } = createHarness('server', {
487
259
  authorization: 'Bearer incoming',
488
260
  });
@@ -515,177 +287,4 @@ describe('requestFactory outbound request contract', () => {
515
287
  accept: ACCEPT_HEADER,
516
288
  });
517
289
  });
518
-
519
- test('reconfiguration removes omitted retry policy', async () => {
520
- const failure = Object.assign(new Error('unavailable'), { status: 503 });
521
- const request = rs.fn(async () => {
522
- throw failure;
523
- });
524
- const { requestFactory } = createHarness('server');
525
-
526
- requestFactory.configure({
527
- request: request as unknown as typeof fetch,
528
- transport: {
529
- retry: {
530
- retries: 1,
531
- baseDelayMs: 1,
532
- maxDelayMs: 1,
533
- jitterRatio: 0,
534
- },
535
- },
536
- });
537
- requestFactory.configure({
538
- request: request as unknown as typeof fetch,
539
- });
540
-
541
- const send = requestFactory.createRequest({
542
- path: REQUEST_PATH,
543
- method: 'GET',
544
- port: PORT,
545
- });
546
-
547
- await expect(send()).rejects.toBe(failure);
548
- expect(request).toHaveBeenCalledTimes(1);
549
- });
550
-
551
- test('reconfiguration restores omitted secured-producer defaults', () => {
552
- const requestId = 'producer-policy-reset';
553
- const { request, requestFactory } = createHarness('server');
554
-
555
- requestFactory.configure({
556
- request: request as unknown as typeof fetch,
557
- requestId,
558
- requireEnvelope: false,
559
- identityBinding: { enabled: false },
560
- operationContract: { enabled: false },
561
- setDomain: () => 'https://producer.example',
562
- });
563
- requestFactory.configure({
564
- request: request as unknown as typeof fetch,
565
- requestId,
566
- });
567
-
568
- const send = requestFactory.createRequest({
569
- path: REQUEST_PATH,
570
- method: 'GET',
571
- port: PORT,
572
- requestId,
573
- });
574
-
575
- expect(() => send()).toThrow('missing_schema_hash');
576
- });
577
-
578
- test('uploader uses an explicitly configured POST retry policy', async () => {
579
- rs.useFakeTimers();
580
- const retryableError = Object.assign(new Error('upload unavailable'), {
581
- status: 503,
582
- });
583
- const response = new Response('{}', { status: 200 });
584
- const onDegraded = rs.fn();
585
- let attempts = 0;
586
- const request = rs.fn(async () => {
587
- attempts += 1;
588
- if (attempts === 1) {
589
- throw retryableError;
590
- }
591
- return response;
592
- });
593
- const { requestFactory } = createHarness('server');
594
-
595
- requestFactory.configure({
596
- request: request as unknown as typeof fetch,
597
- transport: {
598
- retry: {
599
- retries: 1,
600
- baseDelayMs: 5,
601
- maxDelayMs: 5,
602
- jitterRatio: 0,
603
- shouldRetry: ({ method }) => method === 'POST',
604
- },
605
- onDegraded,
606
- },
607
- });
608
- const upload = requestFactory.createUploader({ path: REQUEST_PATH });
609
- const pending = upload({
610
- files: { file: new File(['widget'], 'widget.txt') },
611
- });
612
- const observed = pending.catch(error => error);
613
-
614
- try {
615
- await Promise.resolve();
616
- expect(request).toHaveBeenCalledTimes(1);
617
- await rs.advanceTimersByTimeAsync(5);
618
-
619
- await expect(pending).resolves.toBe(response);
620
- expect(request).toHaveBeenCalledTimes(2);
621
- expect(onDegraded).toHaveBeenCalledWith(
622
- expect.objectContaining({
623
- reason: 'retry',
624
- method: 'POST',
625
- attempt: 1,
626
- maxAttempts: 2,
627
- }),
628
- );
629
- } finally {
630
- await rs.advanceTimersByTimeAsync(1000);
631
- await observed;
632
- rs.useRealTimers();
633
- }
634
- });
635
-
636
- test('uploader timeout aborts transport and emits degraded telemetry', async () => {
637
- rs.useFakeTimers();
638
- const onDegraded = rs.fn();
639
- const cleanupError = new Error('test cleanup');
640
- let rejectRequest: ((error: Error) => void) | undefined;
641
- let inFlightSignal: AbortSignal | undefined;
642
- const request = rs.fn((_url: RequestInfo | URL, init?: RequestInit) => {
643
- inFlightSignal = init?.signal || undefined;
644
- return new Promise((_, reject) => {
645
- rejectRequest = reject;
646
- init?.signal?.addEventListener('abort', () => {
647
- const error = new Error('aborted');
648
- error.name = 'AbortError';
649
- reject(error);
650
- });
651
- });
652
- });
653
- const { requestFactory } = createHarness('server');
654
-
655
- requestFactory.configure({
656
- request: request as unknown as typeof fetch,
657
- transport: {
658
- timeoutMs: 20,
659
- onDegraded,
660
- },
661
- });
662
- const upload = requestFactory.createUploader({ path: REQUEST_PATH });
663
- const pending = upload({
664
- files: { file: new File(['widget'], 'widget.txt') },
665
- });
666
- const observed = pending.catch(error => error);
667
-
668
- try {
669
- await Promise.resolve();
670
- expect(inFlightSignal).toBeDefined();
671
- await rs.advanceTimersByTimeAsync(20);
672
-
673
- await expect(observed).resolves.toMatchObject({ name: 'TimeoutError' });
674
- expect(inFlightSignal?.aborted).toBe(true);
675
- expect(onDegraded).toHaveBeenCalledWith(
676
- expect.objectContaining({
677
- reason: 'timeout',
678
- method: 'POST',
679
- timeoutMs: 20,
680
- }),
681
- );
682
- } finally {
683
- if (!inFlightSignal?.aborted) {
684
- rejectRequest?.(cleanupError);
685
- }
686
- await observed;
687
- await rs.advanceTimersByTimeAsync(1000);
688
- rs.useRealTimers();
689
- }
690
- });
691
290
  });