@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.5 → 3.9.0-ultramodern.7
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/dist/cjs/routerProvider.js +9 -74
- package/dist/esm/routerProvider.mjs +8 -67
- package/dist/esm-node/routerProvider.mjs +8 -67
- package/dist/types/routerProvider.d.ts +2 -24
- package/dist/types/server-config/bffRuntime.d.ts +2 -2
- package/dist/types/server-config/serverTelemetry.d.ts +27 -27
- package/package.json +4 -4
- package/src/routerProvider.ts +14 -194
- package/src/server-config/bffRuntime.ts +2 -2
- package/src/server-config/serverTelemetry.ts +27 -27
- package/tests/buildIdentity.test.ts +0 -19
- package/tests/contextExtensions.test.ts +0 -44
- package/tests/headRuntime.test.tsx +0 -140
- package/tests/rendererHead.test.ts +0 -41
- package/tests/request-policy/browser-policy.test.ts +0 -500
- package/tests/request-policy/client-isolation.test.ts +0 -40
- package/tests/request-policy/node-policy.test.ts +0 -294
- package/tests/request-policy/policyCore.test.ts +0 -121
- package/tests/request-policy/requestContext.test.ts +8 -22
- package/tests/request-policy/requestFactory.outbound-contract.test.ts +109 -510
- package/tests/request-policy/traceparent.test.ts +2 -5
- package/tests/request-policy/transport-retry.test.ts +44 -47
- package/tests/routerProvider.test.ts +2 -11
- package/tests/routerState.test.ts +0 -79
- package/tests/routerStatePlugin.test.ts +1 -46
- package/tests/runtimeContextProjection.test.ts +0 -52
- package/tests/safeFailure.test.ts +10 -28
- package/tests/workspaceEvents.test.ts +31 -201
- package/tests/nativeSafeFailureCompatibility.test.ts +0 -117
- package/tests/prefetchPolicy.test.ts +0 -51
- package/tests/request-policy/identityBinding.test.ts +0 -75
|
@@ -7,6 +7,8 @@ describe('parseTraceparent', () => {
|
|
|
7
7
|
test.each([
|
|
8
8
|
['01', true],
|
|
9
9
|
['00', false],
|
|
10
|
+
['03', true],
|
|
11
|
+
['02', false],
|
|
10
12
|
])('parses a valid traceparent with flags %s', (flags, sampled) => {
|
|
11
13
|
expect(parseTraceparent(`00-${traceId}-${spanId}-${flags}`)).toEqual({
|
|
12
14
|
traceId,
|
|
@@ -27,11 +29,6 @@ describe('parseTraceparent', () => {
|
|
|
27
29
|
});
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
test('reads the sampled bit from arbitrary flag values', () => {
|
|
31
|
-
expect(parseTraceparent(`00-${traceId}-${spanId}-03`)?.sampled).toBe(true);
|
|
32
|
-
expect(parseTraceparent(`00-${traceId}-${spanId}-02`)?.sampled).toBe(false);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
32
|
test('rejects missing, empty, and malformed headers', () => {
|
|
36
33
|
expect(parseTraceparent(undefined)).toBeUndefined();
|
|
37
34
|
expect(parseTraceparent(null)).toBeUndefined();
|
|
@@ -50,7 +50,6 @@ const executeRetryRequest = ({
|
|
|
50
50
|
describe('transport retry behavior', () => {
|
|
51
51
|
test.each([
|
|
52
52
|
'FetchError',
|
|
53
|
-
'TimeoutError',
|
|
54
53
|
])('retries %s up to the configured max then rejects', async errorName => {
|
|
55
54
|
rs.useFakeTimers();
|
|
56
55
|
const errors = [
|
|
@@ -144,36 +143,35 @@ describe('transport retry behavior', () => {
|
|
|
144
143
|
rs.useFakeTimers();
|
|
145
144
|
|
|
146
145
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
const statusCode = retryableStatusCodes[0];
|
|
147
|
+
const retryableError = createStatusError(statusCode);
|
|
148
|
+
const response = { ok: true, status: 200 };
|
|
149
|
+
let callCount = 0;
|
|
150
|
+
const fetcher = rs.fn(async () => {
|
|
151
|
+
callCount += 1;
|
|
152
|
+
if (callCount === 1) {
|
|
153
|
+
throw retryableError;
|
|
154
|
+
}
|
|
155
|
+
return response;
|
|
156
|
+
});
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
158
|
+
const pending = executeRetryRequest({
|
|
159
|
+
fetcher,
|
|
160
|
+
retry: {
|
|
161
|
+
retries: 1,
|
|
162
|
+
baseDelayMs: 5,
|
|
163
|
+
maxDelayMs: 5,
|
|
164
|
+
jitterRatio: 0,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
168
167
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
168
|
+
await flushMicrotasks();
|
|
169
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
170
|
+
await rs.advanceTimersByTimeAsync(4);
|
|
171
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
172
|
+
await rs.advanceTimersByTimeAsync(1);
|
|
173
|
+
await expect(pending).resolves.toBe(response);
|
|
174
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
177
175
|
} finally {
|
|
178
176
|
await rs.advanceTimersByTimeAsync(1000);
|
|
179
177
|
rs.useRealTimers();
|
|
@@ -181,25 +179,24 @@ describe('transport retry behavior', () => {
|
|
|
181
179
|
});
|
|
182
180
|
|
|
183
181
|
test('does not retry non-retryable status codes', async () => {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
182
|
+
const statusCode = nonRetryableStatusCodes[0];
|
|
183
|
+
const error = createStatusError(statusCode);
|
|
184
|
+
const fetcher = rs.fn(async () => {
|
|
185
|
+
throw error;
|
|
186
|
+
});
|
|
189
187
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
188
|
+
await expect(
|
|
189
|
+
executeRetryRequest({
|
|
190
|
+
fetcher,
|
|
191
|
+
retry: {
|
|
192
|
+
retries: 2,
|
|
193
|
+
baseDelayMs: 1,
|
|
194
|
+
maxDelayMs: 1,
|
|
195
|
+
jitterRatio: 0,
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
).rejects.toBe(error);
|
|
199
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
203
200
|
});
|
|
204
201
|
|
|
205
202
|
test('uses a custom shouldRetry predicate instead of default rules', async () => {
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createRouterPlugin,
|
|
3
|
-
registerRouterProvider,
|
|
4
|
-
unsafe_resetRouterProvidersForTesting,
|
|
5
|
-
} from '../src/routerProvider';
|
|
6
|
-
|
|
7
|
-
afterEach(() => unsafe_resetRouterProvidersForTesting());
|
|
1
|
+
import { createRouterPlugin } from '../src/routerProvider';
|
|
8
2
|
|
|
9
3
|
test('selects the injected app-local factory from runtime config and preserves native hook objects', () => {
|
|
10
4
|
const registryHooks = { afterCreate: { call() {} } };
|
|
@@ -24,9 +18,7 @@ test('selects the injected app-local factory from runtime config and preserves n
|
|
|
24
18
|
expect(defaultFactory).not.toHaveBeenCalled();
|
|
25
19
|
});
|
|
26
20
|
|
|
27
|
-
test('
|
|
28
|
-
const foreign = rstest.fn(() => ({}));
|
|
29
|
-
registerRouterProvider('foreign', foreign);
|
|
21
|
+
test('rejects missing local providers', () => {
|
|
30
22
|
const factory = createRouterPlugin({
|
|
31
23
|
defaultProvider: { name: 'native', factory: () => ({}) },
|
|
32
24
|
registryHooks: {},
|
|
@@ -34,5 +26,4 @@ test('does not resolve missing local providers through the global compatibility
|
|
|
34
26
|
expect(() =>
|
|
35
27
|
factory({ framework: 'foreign' }).setup({ getRuntimeConfig: () => ({}) }),
|
|
36
28
|
).toThrow(/app-owned router provider realm/);
|
|
37
|
-
expect(foreign).not.toHaveBeenCalled();
|
|
38
29
|
});
|
|
@@ -1,49 +1,14 @@
|
|
|
1
|
-
import { createRuntimeContextExtension } from '../src/contextExtensions';
|
|
2
1
|
import {
|
|
3
2
|
applyRouterRuntimeState,
|
|
4
3
|
applyRouterServerPrepareResult,
|
|
5
4
|
cleanupRouterRuntimeState,
|
|
6
|
-
createRouterRuntimeState,
|
|
7
|
-
createRouterServerSnapshot,
|
|
8
5
|
getRouterHydrationScripts,
|
|
9
6
|
getRouterMatchedRouteIds,
|
|
10
7
|
getRouterRuntimeState,
|
|
11
8
|
getRouterServerSnapshot,
|
|
12
|
-
type InternalRouterRuntimeState,
|
|
13
9
|
} from '../src/routerState';
|
|
14
10
|
|
|
15
11
|
describe('router state primitives', () => {
|
|
16
|
-
it('normalizes snapshots without changing explicit values or input objects', () => {
|
|
17
|
-
const matches = [
|
|
18
|
-
{ routeId: 'route-a', assetRouteId: 'asset-a' },
|
|
19
|
-
{ routeId: 'route-b' },
|
|
20
|
-
];
|
|
21
|
-
const snapshot = {
|
|
22
|
-
framework: 'custom',
|
|
23
|
-
hydrationScript: '<script>first()</script>',
|
|
24
|
-
matches,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
expect(createRouterServerSnapshot(snapshot)).toEqual({
|
|
28
|
-
...snapshot,
|
|
29
|
-
hydrationScripts: ['<script>first()</script>'],
|
|
30
|
-
matchedRouteIds: ['asset-a', 'route-b'],
|
|
31
|
-
});
|
|
32
|
-
expect(snapshot).not.toHaveProperty('hydrationScripts');
|
|
33
|
-
expect(snapshot).not.toHaveProperty('matchedRouteIds');
|
|
34
|
-
expect(
|
|
35
|
-
createRouterServerSnapshot({
|
|
36
|
-
...snapshot,
|
|
37
|
-
hydrationScripts: ['<script>explicit()</script>'],
|
|
38
|
-
matchedRouteIds: [],
|
|
39
|
-
}),
|
|
40
|
-
).toMatchObject({
|
|
41
|
-
hydrationScript: '<script>first()</script>',
|
|
42
|
-
hydrationScripts: ['<script>explicit()</script>'],
|
|
43
|
-
matchedRouteIds: [],
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
12
|
it('preserves snapshot precedence and a captured snapshot across client state updates', () => {
|
|
48
13
|
const context = { requestId: 'one' };
|
|
49
14
|
expect(
|
|
@@ -79,27 +44,6 @@ describe('router state primitives', () => {
|
|
|
79
44
|
expect(getRouterMatchedRouteIds(context)).toEqual(['server-route']);
|
|
80
45
|
});
|
|
81
46
|
|
|
82
|
-
it('shares interned slots across accessors and context spreads without serializing state', () => {
|
|
83
|
-
const context = { requestId: 'one' };
|
|
84
|
-
const state: InternalRouterRuntimeState = {
|
|
85
|
-
framework: 'custom',
|
|
86
|
-
instance: { private: true },
|
|
87
|
-
hydrationScript: 'legacy',
|
|
88
|
-
matches: [{ routeId: 'route', assetRouteId: 'asset' }],
|
|
89
|
-
};
|
|
90
|
-
const otherAccessor =
|
|
91
|
-
createRuntimeContextExtension<InternalRouterRuntimeState>(
|
|
92
|
-
'@modern-js/runtime:router-runtime-state',
|
|
93
|
-
);
|
|
94
|
-
otherAccessor.set(context, state);
|
|
95
|
-
expect(getRouterRuntimeState({ ...context })).toBe(state);
|
|
96
|
-
expect(getRouterHydrationScripts(context)).toEqual(['legacy']);
|
|
97
|
-
expect(getRouterMatchedRouteIds(context)).toEqual(['asset']);
|
|
98
|
-
expect(Object.keys(context)).toEqual(['requestId']);
|
|
99
|
-
expect(JSON.stringify(context)).toBe('{"requestId":"one"}');
|
|
100
|
-
expect(getRouterRuntimeState({})).toBeUndefined();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
47
|
it('applies server prepare overrides and awaits cleanup without propagating failures', async () => {
|
|
104
48
|
const context = {};
|
|
105
49
|
const stateCleanup = rstest.fn();
|
|
@@ -131,27 +75,4 @@ describe('router state primitives', () => {
|
|
|
131
75
|
await expect(cleanupRouterRuntimeState(context)).resolves.toBeUndefined();
|
|
132
76
|
await expect(cleanupRouterRuntimeState({})).resolves.toBeUndefined();
|
|
133
77
|
});
|
|
134
|
-
|
|
135
|
-
it('keeps empty state empty and derives server data only when present', () => {
|
|
136
|
-
expect(createRouterRuntimeState({ framework: 'custom' })).toEqual({
|
|
137
|
-
framework: 'custom',
|
|
138
|
-
});
|
|
139
|
-
expect(getRouterHydrationScripts({})).toEqual([]);
|
|
140
|
-
expect(getRouterMatchedRouteIds({})).toBeUndefined();
|
|
141
|
-
expect(
|
|
142
|
-
createRouterRuntimeState({
|
|
143
|
-
framework: 'custom',
|
|
144
|
-
matches: [{ routeId: 'root' }],
|
|
145
|
-
hydrationScripts: ['first', 'second'],
|
|
146
|
-
}),
|
|
147
|
-
).toMatchObject({
|
|
148
|
-
hydrationScript: 'first',
|
|
149
|
-
matchedRouteIds: ['root'],
|
|
150
|
-
serverSnapshot: {
|
|
151
|
-
framework: 'custom',
|
|
152
|
-
hydrationScripts: ['first', 'second'],
|
|
153
|
-
matchedRouteIds: ['root'],
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
78
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
applyRouterRuntimeState,
|
|
3
2
|
getRouterRuntimeState,
|
|
4
3
|
getRouterServerSnapshot,
|
|
5
4
|
} from '../src/routerState';
|
|
@@ -24,8 +23,7 @@ function install() {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
test('keeps injected hooks and per-context policies stable without sharing app values', () => {
|
|
27
|
-
const {
|
|
28
|
-
expect(plugin.registryHooks).toBe(registryHooks);
|
|
26
|
+
const { beforeRender } = install();
|
|
29
27
|
const first: any = {};
|
|
30
28
|
const second: any = {};
|
|
31
29
|
beforeRender(first);
|
|
@@ -36,25 +34,6 @@ test('keeps injected hooks and per-context policies stable without sharing app v
|
|
|
36
34
|
expect(second.linkPrefetchPolicy).not.toBe(policy);
|
|
37
35
|
});
|
|
38
36
|
|
|
39
|
-
test('stores the exact native client instance and retains an earlier server snapshot', () => {
|
|
40
|
-
const { afterCreate } = install();
|
|
41
|
-
const context = {};
|
|
42
|
-
applyRouterRuntimeState(context, {
|
|
43
|
-
framework: 'react-router',
|
|
44
|
-
serverSnapshot: { statusCode: 207, matchedRouteIds: ['server'] },
|
|
45
|
-
});
|
|
46
|
-
const router = { navigate() {} };
|
|
47
|
-
afterCreate({
|
|
48
|
-
framework: 'react-router',
|
|
49
|
-
phase: 'client-create',
|
|
50
|
-
runtimeContext: context,
|
|
51
|
-
router,
|
|
52
|
-
basename: '/app',
|
|
53
|
-
});
|
|
54
|
-
expect(getRouterRuntimeState(context)?.instance).toBe(router);
|
|
55
|
-
expect(getRouterServerSnapshot(context)?.statusCode).toBe(207);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
37
|
test('projects loader data, errors and valid route ids from injected SSR input', () => {
|
|
59
38
|
const { afterCreate } = install();
|
|
60
39
|
const errors = { route: new Error('route error') };
|
|
@@ -82,27 +61,3 @@ test('projects loader data, errors and valid route ids from injected SSR input',
|
|
|
82
61
|
routerData: { loaderData: context.routerContext.loaderData, errors },
|
|
83
62
|
});
|
|
84
63
|
});
|
|
85
|
-
|
|
86
|
-
test('leaves other providers and incomplete native preparation untouched', () => {
|
|
87
|
-
const { afterCreate } = install();
|
|
88
|
-
const context = {};
|
|
89
|
-
applyRouterRuntimeState(context, {
|
|
90
|
-
framework: 'tanstack',
|
|
91
|
-
hydrationScripts: ['<script>hydrate()</script>'],
|
|
92
|
-
matches: [{ routeId: 'r', assetRouteId: 'asset' }],
|
|
93
|
-
});
|
|
94
|
-
const state = getRouterRuntimeState(context);
|
|
95
|
-
afterCreate({
|
|
96
|
-
framework: 'tanstack',
|
|
97
|
-
phase: 'ssr-prepare',
|
|
98
|
-
runtimeContext: context,
|
|
99
|
-
});
|
|
100
|
-
expect(getRouterRuntimeState(context)).toBe(state);
|
|
101
|
-
const incomplete = {};
|
|
102
|
-
afterCreate({
|
|
103
|
-
framework: 'react-router',
|
|
104
|
-
phase: 'ssr-prepare',
|
|
105
|
-
runtimeContext: incomplete,
|
|
106
|
-
});
|
|
107
|
-
expect(getRouterRuntimeState(incomplete)).toBeUndefined();
|
|
108
|
-
});
|
|
@@ -59,66 +59,14 @@ describe('fork runtime context projection', () => {
|
|
|
59
59
|
expect(internal.requestContext).toBe(publicValue.requestContext);
|
|
60
60
|
expect(internal.context).toBe(internal.requestContext);
|
|
61
61
|
expect(publicValue.context).toBe(internal.requestContext);
|
|
62
|
-
expect(internal.requestContext.request).toEqual({
|
|
63
|
-
params: { category: 'compact' },
|
|
64
|
-
pathname: '/tractors',
|
|
65
|
-
query: { sort: 'price' },
|
|
66
|
-
headers: { accept: 'text/html' },
|
|
67
|
-
host: 'example.test',
|
|
68
|
-
url: 'https://example.test/tractors?sort=price',
|
|
69
|
-
userAgent: undefined,
|
|
70
|
-
cookie: undefined,
|
|
71
|
-
referer: undefined,
|
|
72
|
-
});
|
|
73
|
-
expect(internal.requestContext.response).toEqual({
|
|
74
|
-
locals: response.locals,
|
|
75
|
-
});
|
|
76
62
|
expect(internal.requestContext.response.locals).toBe(response.locals);
|
|
77
|
-
expect(Object.keys(internal.requestContext.response)).toEqual(['locals']);
|
|
78
|
-
expect(typeof internal.requestContext.response.setHeader).toBe('function');
|
|
79
|
-
expect(typeof internal.requestContext.response.status).toBe('function');
|
|
80
63
|
expect(context.ssrContext.response).toBe(response);
|
|
81
|
-
expect(Object.keys(response)).toEqual(['setHeader', 'status', 'locals']);
|
|
82
64
|
expect(extension.get(context)).toBe(state);
|
|
83
65
|
// Internal RSC copy behavior is retained pending separate Flight proof.
|
|
84
66
|
expect(extension.get(internal)).toBe(state);
|
|
85
67
|
expect(extension.get(publicValue)).toBeUndefined();
|
|
86
68
|
});
|
|
87
69
|
|
|
88
|
-
it('provides the existing empty RSC request fallback when no SSR context exists', () => {
|
|
89
|
-
const context = { isBrowser: false };
|
|
90
|
-
const result = projectRuntimeContext(
|
|
91
|
-
{ internalContext: context, publicContext: { ...context } },
|
|
92
|
-
{ context, isRsc: true },
|
|
93
|
-
);
|
|
94
|
-
expect(result.internalContext).toMatchObject({
|
|
95
|
-
context: {
|
|
96
|
-
request: {
|
|
97
|
-
params: {},
|
|
98
|
-
pathname: '',
|
|
99
|
-
query: {},
|
|
100
|
-
headers: {},
|
|
101
|
-
host: '',
|
|
102
|
-
url: '',
|
|
103
|
-
},
|
|
104
|
-
response: { locals: {} },
|
|
105
|
-
},
|
|
106
|
-
requestContext: {
|
|
107
|
-
request: {
|
|
108
|
-
params: {},
|
|
109
|
-
pathname: '',
|
|
110
|
-
query: {},
|
|
111
|
-
headers: {},
|
|
112
|
-
host: '',
|
|
113
|
-
url: '',
|
|
114
|
-
},
|
|
115
|
-
response: { locals: {} },
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
expect(result.internalContext).not.toHaveProperty('ssrContext');
|
|
119
|
-
expect(context).toEqual({ isBrowser: false });
|
|
120
|
-
});
|
|
121
|
-
|
|
122
70
|
it('does not apply server request projection to an RSC browser context', () => {
|
|
123
71
|
const context = {
|
|
124
72
|
isBrowser: true,
|
|
@@ -10,9 +10,7 @@ const readBody = async (response: Response) =>
|
|
|
10
10
|
|
|
11
11
|
describe('safe failure responses', () => {
|
|
12
12
|
it.each([
|
|
13
|
-
[404, 'REQUEST_FAILED', 'Request failed'],
|
|
14
13
|
[500, 'INTERNAL_SERVER_ERROR', 'Internal Server Error'],
|
|
15
|
-
[503, 'SERVICE_UNAVAILABLE', 'Service Unavailable'],
|
|
16
14
|
])('redacts a %i failure through every response surface', async (status, code, message) => {
|
|
17
15
|
const error = Object.assign(new Error('database-password'), {
|
|
18
16
|
status,
|
|
@@ -54,23 +52,6 @@ describe('safe failure responses', () => {
|
|
|
54
52
|
|
|
55
53
|
it.each([
|
|
56
54
|
['delay seconds text', ' 120 ', '120'],
|
|
57
|
-
['leading-zero delay text', '000120', '120'],
|
|
58
|
-
['delay seconds number', 2.1, '3'],
|
|
59
|
-
[
|
|
60
|
-
'largest safe delay number',
|
|
61
|
-
Number.MAX_SAFE_INTEGER,
|
|
62
|
-
String(Number.MAX_SAFE_INTEGER),
|
|
63
|
-
],
|
|
64
|
-
[
|
|
65
|
-
'largest safe delay text',
|
|
66
|
-
String(Number.MAX_SAFE_INTEGER),
|
|
67
|
-
String(Number.MAX_SAFE_INTEGER),
|
|
68
|
-
],
|
|
69
|
-
[
|
|
70
|
-
'canonical HTTP-date text',
|
|
71
|
-
'Sun, 30 Aug 2026 20:00:00 GMT',
|
|
72
|
-
'Sun, 30 Aug 2026 20:00:00 GMT',
|
|
73
|
-
],
|
|
74
55
|
[
|
|
75
56
|
'valid Date object',
|
|
76
57
|
new Date(Date.UTC(2026, 7, 30, 20, 0, 0)),
|
|
@@ -81,6 +62,16 @@ describe('safe failure responses', () => {
|
|
|
81
62
|
expect(result.headers['Retry-After']).toBe(expected);
|
|
82
63
|
});
|
|
83
64
|
|
|
65
|
+
it('derives Retry-After from a retryAfterSeconds error property', () => {
|
|
66
|
+
const error = Object.assign(new Error('maintenance'), {
|
|
67
|
+
status: 503,
|
|
68
|
+
retryAfterSeconds: 120,
|
|
69
|
+
});
|
|
70
|
+
expect(createSafeFailureHttpResult(error).headers['Retry-After']).toBe(
|
|
71
|
+
'120',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
84
75
|
it('converts Retry-After milliseconds to canonical delay seconds', () => {
|
|
85
76
|
expect(
|
|
86
77
|
createSafeFailureHttpResult({ status: 503, retryAfterMs: 2500 }).headers[
|
|
@@ -91,15 +82,6 @@ describe('safe failure responses', () => {
|
|
|
91
82
|
|
|
92
83
|
it.each([
|
|
93
84
|
['a header injection payload', '120\r\nX-Injected: true'],
|
|
94
|
-
['arbitrary text', 'after maintenance'],
|
|
95
|
-
['an exponent string', '1e3'],
|
|
96
|
-
['a decimal string', '2.1'],
|
|
97
|
-
['a negative string', '-1'],
|
|
98
|
-
['an unsafe integer string', '9007199254740992'],
|
|
99
|
-
['an unsafe number', 1e21],
|
|
100
|
-
['infinity', Number.POSITIVE_INFINITY],
|
|
101
|
-
['a noncanonical date', 'Sunday, 30-Aug-26 20:00:00 GMT'],
|
|
102
|
-
['an invalid date string', 'Wed, 99 Jun 2026 25:61:61 GMT'],
|
|
103
85
|
['an invalid Date object', new Date(Number.NaN)],
|
|
104
86
|
])('rejects %s as Retry-After', (_, retryAfter) => {
|
|
105
87
|
const result = createSafeFailureHttpResult({ status: 503, retryAfter });
|