@beignet/core 0.0.27 → 0.0.28
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 +8 -0
- package/README.md +27 -6
- package/dist/idempotency/index.d.ts +2 -2
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +17 -9
- package/dist/idempotency/index.js.map +1 -1
- package/dist/server/hooks/idempotency.d.ts +7 -6
- package/dist/server/hooks/idempotency.d.ts.map +1 -1
- package/dist/server/hooks/idempotency.js +16 -11
- package/dist/server/hooks/idempotency.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +5 -4
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +5 -4
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/http.d.ts +7 -5
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/internal-hooks.d.ts +25 -0
- package/dist/server/internal-hooks.d.ts.map +1 -0
- package/dist/server/internal-hooks.js +5 -0
- package/dist/server/internal-hooks.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +99 -40
- package/dist/server/server.js.map +1 -1
- package/package.json +1 -1
- package/src/idempotency/index.ts +21 -13
- package/src/providers/instrumentation.ts +13 -0
- package/src/server/hooks/idempotency.ts +28 -10
- package/src/server/hooks/rate-limit.ts +5 -4
- package/src/server/http.ts +31 -5
- package/src/server/index.ts +2 -0
- package/src/server/instrumentation.ts +10 -1
- package/src/server/internal-hooks.ts +49 -0
- package/src/server/server.ts +285 -64
package/src/server/server.ts
CHANGED
|
@@ -53,6 +53,7 @@ import type {
|
|
|
53
53
|
HttpRequestLike,
|
|
54
54
|
HttpResponse,
|
|
55
55
|
HttpResponseLike,
|
|
56
|
+
RequestStageTimings,
|
|
56
57
|
ResolvedRoute,
|
|
57
58
|
RouteHook,
|
|
58
59
|
ServerCaughtErrorHook,
|
|
@@ -61,6 +62,11 @@ import type {
|
|
|
61
62
|
} from "./http.js";
|
|
62
63
|
import type { ServerInstrumentationOptions } from "./instrumentation.js";
|
|
63
64
|
import { createServerInstrumentation } from "./instrumentation.js";
|
|
65
|
+
import {
|
|
66
|
+
getResponseFinalizerHook,
|
|
67
|
+
type ResponseFinalizerResponseOwner,
|
|
68
|
+
type ResponseFinalizerValidationState,
|
|
69
|
+
} from "./internal-hooks.js";
|
|
64
70
|
import {
|
|
65
71
|
loadProviderConfig,
|
|
66
72
|
parseStandardSchema,
|
|
@@ -399,6 +405,30 @@ interface RouteBuilder<Ctx, C extends HttpContractConfig> {
|
|
|
399
405
|
) => (req: HttpRequestLike) => Promise<HttpResponse>;
|
|
400
406
|
}
|
|
401
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Identity and hook metadata for a raw route.
|
|
410
|
+
*
|
|
411
|
+
* Raw routes are mounted by the adapter at their own path, so `method` and
|
|
412
|
+
* `path` describe the route to hooks, instrumentation, and error mapping
|
|
413
|
+
* instead of driving routing. `metadata` feeds metadata-driven hooks exactly
|
|
414
|
+
* like contract metadata does, for example `rateLimit` and `idempotency`.
|
|
415
|
+
*/
|
|
416
|
+
export type RawRouteInit = {
|
|
417
|
+
name: string;
|
|
418
|
+
method: HttpContractConfig["method"];
|
|
419
|
+
path: string;
|
|
420
|
+
metadata?: HttpContractConfig["metadata"];
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Builder returned by `server.rawRoute(...)`.
|
|
425
|
+
*/
|
|
426
|
+
export interface RawRouteBuilder<Ctx> {
|
|
427
|
+
handle: (
|
|
428
|
+
fn: Handler<Ctx, HttpContractConfig>,
|
|
429
|
+
) => (req: HttpRequestLike) => Promise<HttpResponse>;
|
|
430
|
+
}
|
|
431
|
+
|
|
402
432
|
/**
|
|
403
433
|
* Runtime server object returned by `createServer(...)`.
|
|
404
434
|
*/
|
|
@@ -417,6 +447,21 @@ export interface ServerInstance<
|
|
|
417
447
|
route: <CLike extends ContractLike>(
|
|
418
448
|
contractLike: CLike,
|
|
419
449
|
) => RouteBuilder<Ctx, ResolveContract<CLike>>;
|
|
450
|
+
/**
|
|
451
|
+
* Build a handler for a route that cannot be a contract — webhooks,
|
|
452
|
+
* third-party auth callbacks, streaming endpoints — that still runs the
|
|
453
|
+
* whole server pipeline: correlation, `onRequest`, route hooks,
|
|
454
|
+
* `beforeHandle`, `beforeSend`, `afterSend`, context creation,
|
|
455
|
+
* instrumentation, and framework error mapping.
|
|
456
|
+
*
|
|
457
|
+
* Request parsing and validation are skipped and the request body is left
|
|
458
|
+
* unconsumed, so the handler owns body reading — for example webhook
|
|
459
|
+
* signature verification over the exact raw bytes. Metadata-driven hooks
|
|
460
|
+
* such as rate limiting and idempotency read `init.metadata`. Raw routes
|
|
461
|
+
* are not added to the route registry; mount the returned handler at the
|
|
462
|
+
* route's own path.
|
|
463
|
+
*/
|
|
464
|
+
rawRoute: (init: RawRouteInit) => RawRouteBuilder<Ctx>;
|
|
420
465
|
/**
|
|
421
466
|
* Build a fully assembled request context from a framework-neutral request.
|
|
422
467
|
*
|
|
@@ -475,7 +520,7 @@ type ExecutionResult<Ctx> = {
|
|
|
475
520
|
owner?: ResponseOwner;
|
|
476
521
|
};
|
|
477
522
|
|
|
478
|
-
type ResponseOwner =
|
|
523
|
+
type ResponseOwner = ResponseFinalizerResponseOwner;
|
|
479
524
|
|
|
480
525
|
type CompiledPath = {
|
|
481
526
|
keys: string[];
|
|
@@ -1125,6 +1170,12 @@ function createRequestExecutor<
|
|
|
1125
1170
|
routeHooks: readonly RouteHook<unknown, object>[] = [],
|
|
1126
1171
|
optionsOverrides?: {
|
|
1127
1172
|
skipRoutePreparation?: boolean;
|
|
1173
|
+
/**
|
|
1174
|
+
* Run the route through the full hook pipeline without contract
|
|
1175
|
+
* preparation: no query/path/header/body parsing or validation, and the
|
|
1176
|
+
* request body is left unconsumed for the handler.
|
|
1177
|
+
*/
|
|
1178
|
+
rawRoute?: boolean;
|
|
1128
1179
|
},
|
|
1129
1180
|
): (
|
|
1130
1181
|
target: ExecutionTarget<Ctx, C>,
|
|
@@ -1146,6 +1197,27 @@ function createRequestExecutor<
|
|
|
1146
1197
|
let headersValue: HandlerArgs<Ctx, C>["headers"] | undefined;
|
|
1147
1198
|
let bodyValue: HandlerArgs<Ctx, C>["body"] | undefined;
|
|
1148
1199
|
const startedAt = Date.now();
|
|
1200
|
+
// Stage timings accumulate because retries re-enter the send phase and
|
|
1201
|
+
// beforeHandle spans the route-hook and server-hook loops.
|
|
1202
|
+
const stages: RequestStageTimings = {
|
|
1203
|
+
onRequestMs: 0,
|
|
1204
|
+
parseMs: 0,
|
|
1205
|
+
contextMs: 0,
|
|
1206
|
+
beforeHandleMs: 0,
|
|
1207
|
+
handlerMs: 0,
|
|
1208
|
+
sendMs: 0,
|
|
1209
|
+
};
|
|
1210
|
+
const timeStage = async <T>(
|
|
1211
|
+
stage: keyof RequestStageTimings,
|
|
1212
|
+
run: () => Promise<T> | T,
|
|
1213
|
+
): Promise<T> => {
|
|
1214
|
+
const stageStartedAt = performance.now();
|
|
1215
|
+
try {
|
|
1216
|
+
return await run();
|
|
1217
|
+
} finally {
|
|
1218
|
+
stages[stage] += performance.now() - stageStartedAt;
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1149
1221
|
|
|
1150
1222
|
const resolveErrorResult = async (
|
|
1151
1223
|
error: unknown,
|
|
@@ -1485,8 +1557,64 @@ function createRequestExecutor<
|
|
|
1485
1557
|
}
|
|
1486
1558
|
};
|
|
1487
1559
|
|
|
1560
|
+
const applyResponseFinalizerHooks = async (
|
|
1561
|
+
initialResult: ExecutionResult<Ctx>,
|
|
1562
|
+
allowRetry: boolean,
|
|
1563
|
+
responseValidation: ResponseFinalizerValidationState,
|
|
1564
|
+
): Promise<ExecutionResult<Ctx>> => {
|
|
1565
|
+
const response = normalizeHttpResponse(initialResult.response);
|
|
1566
|
+
const native = isWebResponse(response);
|
|
1567
|
+
const owner = responseOwnerFor(response, initialResult.owner);
|
|
1568
|
+
|
|
1569
|
+
try {
|
|
1570
|
+
for (const hook of hooks) {
|
|
1571
|
+
const finalizer = getResponseFinalizerHook<Ctx>(hook);
|
|
1572
|
+
if (!finalizer) continue;
|
|
1573
|
+
await finalizer({
|
|
1574
|
+
req,
|
|
1575
|
+
ctx: initialResult.ctx,
|
|
1576
|
+
contract,
|
|
1577
|
+
path: pathValue,
|
|
1578
|
+
query: queryValue,
|
|
1579
|
+
headers: headersValue,
|
|
1580
|
+
body: bodyValue,
|
|
1581
|
+
response: responseForHooks(response),
|
|
1582
|
+
error: initialResult.error,
|
|
1583
|
+
native: native ? true : undefined,
|
|
1584
|
+
owner,
|
|
1585
|
+
responseValidation,
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
...initialResult,
|
|
1591
|
+
response,
|
|
1592
|
+
};
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
const mapped = await resolveErrorResult(
|
|
1595
|
+
error,
|
|
1596
|
+
initialResult.ctx,
|
|
1597
|
+
pathValue,
|
|
1598
|
+
queryValue,
|
|
1599
|
+
headersValue,
|
|
1600
|
+
bodyValue,
|
|
1601
|
+
{ owner: "framework" },
|
|
1602
|
+
);
|
|
1603
|
+
if (!allowRetry) {
|
|
1604
|
+
return mapped;
|
|
1605
|
+
}
|
|
1606
|
+
const transformed = await applyTransformHooks(mapped, true);
|
|
1607
|
+
return applyResponseFinalizerHooks(
|
|
1608
|
+
transformed,
|
|
1609
|
+
false,
|
|
1610
|
+
"not-applicable",
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1488
1615
|
let result: ExecutionResult<Ctx> | undefined;
|
|
1489
1616
|
|
|
1617
|
+
const onRequestStartedAt = performance.now();
|
|
1490
1618
|
for (const hook of hooks) {
|
|
1491
1619
|
if (!hook.onRequest) continue;
|
|
1492
1620
|
try {
|
|
@@ -1517,14 +1645,14 @@ function createRequestExecutor<
|
|
|
1517
1645
|
break;
|
|
1518
1646
|
}
|
|
1519
1647
|
}
|
|
1648
|
+
stages.onRequestMs = performance.now() - onRequestStartedAt;
|
|
1520
1649
|
|
|
1521
1650
|
if (!result) {
|
|
1522
1651
|
if (optionsOverrides?.skipRoutePreparation) {
|
|
1523
1652
|
let createdCtx!: Ctx;
|
|
1524
1653
|
try {
|
|
1525
|
-
createdCtx = await
|
|
1526
|
-
req,
|
|
1527
|
-
contract,
|
|
1654
|
+
createdCtx = await timeStage("contextMs", () =>
|
|
1655
|
+
contextRuntime.createRequestContext(req, contract),
|
|
1528
1656
|
);
|
|
1529
1657
|
baseCtx = createdCtx;
|
|
1530
1658
|
} catch (error) {
|
|
@@ -1544,15 +1672,17 @@ function createRequestExecutor<
|
|
|
1544
1672
|
result = {
|
|
1545
1673
|
ctx: createdCtx,
|
|
1546
1674
|
response: normalizeHttpResponse(
|
|
1547
|
-
await
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1675
|
+
await timeStage("handlerMs", () =>
|
|
1676
|
+
userHandler({
|
|
1677
|
+
req,
|
|
1678
|
+
ctx: createdCtx,
|
|
1679
|
+
contract,
|
|
1680
|
+
path: {} as HandlerArgs<Ctx, C>["path"],
|
|
1681
|
+
query: {} as HandlerArgs<Ctx, C>["query"],
|
|
1682
|
+
headers: rawHeaders as HandlerArgs<Ctx, C>["headers"],
|
|
1683
|
+
body: undefined as HandlerArgs<Ctx, C>["body"],
|
|
1684
|
+
}),
|
|
1685
|
+
),
|
|
1556
1686
|
),
|
|
1557
1687
|
owner: "framework",
|
|
1558
1688
|
};
|
|
@@ -1569,6 +1699,7 @@ function createRequestExecutor<
|
|
|
1569
1699
|
}
|
|
1570
1700
|
}
|
|
1571
1701
|
} else {
|
|
1702
|
+
const parseStartedAt = performance.now();
|
|
1572
1703
|
const rawQuery: Record<string, string | string[]> = {};
|
|
1573
1704
|
for (const key of new Set(url.searchParams.keys())) {
|
|
1574
1705
|
const values = url.searchParams.getAll(key);
|
|
@@ -1641,7 +1772,9 @@ function createRequestExecutor<
|
|
|
1641
1772
|
|
|
1642
1773
|
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
1643
1774
|
let body: any;
|
|
1644
|
-
|
|
1775
|
+
// Raw routes own body consumption: the handler reads `req` itself,
|
|
1776
|
+
// for example to verify a webhook signature over the exact bytes.
|
|
1777
|
+
if (!result && !optionsOverrides?.rawRoute) {
|
|
1645
1778
|
try {
|
|
1646
1779
|
body = await parseBody(req, maxRequestBodyBytes);
|
|
1647
1780
|
} catch (error) {
|
|
@@ -1708,6 +1841,8 @@ function createRequestExecutor<
|
|
|
1708
1841
|
}
|
|
1709
1842
|
}
|
|
1710
1843
|
|
|
1844
|
+
stages.parseMs = performance.now() - parseStartedAt;
|
|
1845
|
+
|
|
1711
1846
|
if (!result) {
|
|
1712
1847
|
pathValue = path;
|
|
1713
1848
|
queryValue = query;
|
|
@@ -1716,9 +1851,8 @@ function createRequestExecutor<
|
|
|
1716
1851
|
|
|
1717
1852
|
let createdCtx!: Ctx;
|
|
1718
1853
|
try {
|
|
1719
|
-
createdCtx = await
|
|
1720
|
-
req,
|
|
1721
|
-
contract,
|
|
1854
|
+
createdCtx = await timeStage("contextMs", () =>
|
|
1855
|
+
contextRuntime.createRequestContext(req, contract),
|
|
1722
1856
|
);
|
|
1723
1857
|
baseCtx = createdCtx;
|
|
1724
1858
|
} catch (error) {
|
|
@@ -1745,10 +1879,10 @@ function createRequestExecutor<
|
|
|
1745
1879
|
};
|
|
1746
1880
|
|
|
1747
1881
|
let currentCtx = createdCtx;
|
|
1748
|
-
|
|
1749
|
-
|
|
1882
|
+
const beforeHandleStartedAt = performance.now();
|
|
1883
|
+
for (const hook of routeHooks) {
|
|
1750
1884
|
try {
|
|
1751
|
-
const
|
|
1885
|
+
const additions = await hook.resolve({
|
|
1752
1886
|
req,
|
|
1753
1887
|
ctx: currentCtx,
|
|
1754
1888
|
contract,
|
|
@@ -1757,33 +1891,11 @@ function createRequestExecutor<
|
|
|
1757
1891
|
headers,
|
|
1758
1892
|
body,
|
|
1759
1893
|
});
|
|
1760
|
-
if (
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
};
|
|
1766
|
-
break;
|
|
1767
|
-
}
|
|
1768
|
-
if (isHttpResponseLike(hookResult)) {
|
|
1769
|
-
result = {
|
|
1770
|
-
ctx: currentCtx,
|
|
1771
|
-
response: normalizeResponse(hookResult),
|
|
1772
|
-
owner: "framework",
|
|
1773
|
-
};
|
|
1774
|
-
break;
|
|
1775
|
-
}
|
|
1776
|
-
if (hookResult?.ctx !== undefined) {
|
|
1777
|
-
currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
|
|
1778
|
-
}
|
|
1779
|
-
if (hookResult?.response) {
|
|
1780
|
-
const response = normalizeHttpResponse(hookResult.response);
|
|
1781
|
-
result = {
|
|
1782
|
-
ctx: currentCtx,
|
|
1783
|
-
response,
|
|
1784
|
-
owner: responseOwnerFor(response, "framework"),
|
|
1785
|
-
};
|
|
1786
|
-
break;
|
|
1894
|
+
if (additions && typeof additions === "object") {
|
|
1895
|
+
currentCtx = contextRuntime.finalizeContext({
|
|
1896
|
+
...(currentCtx as object),
|
|
1897
|
+
...additions,
|
|
1898
|
+
} as Ctx);
|
|
1787
1899
|
}
|
|
1788
1900
|
} catch (error) {
|
|
1789
1901
|
result = await resolveErrorResult(
|
|
@@ -1800,9 +1912,10 @@ function createRequestExecutor<
|
|
|
1800
1912
|
}
|
|
1801
1913
|
|
|
1802
1914
|
if (!result) {
|
|
1803
|
-
for (const hook of
|
|
1915
|
+
for (const hook of hooks) {
|
|
1916
|
+
if (!hook.beforeHandle) continue;
|
|
1804
1917
|
try {
|
|
1805
|
-
const
|
|
1918
|
+
const hookResult = await hook.beforeHandle({
|
|
1806
1919
|
req,
|
|
1807
1920
|
ctx: currentCtx,
|
|
1808
1921
|
contract,
|
|
@@ -1811,11 +1924,37 @@ function createRequestExecutor<
|
|
|
1811
1924
|
headers,
|
|
1812
1925
|
body,
|
|
1813
1926
|
});
|
|
1814
|
-
if (
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1927
|
+
if (isWebResponse(hookResult)) {
|
|
1928
|
+
result = {
|
|
1929
|
+
ctx: currentCtx,
|
|
1930
|
+
response: hookResult,
|
|
1931
|
+
owner: "transport",
|
|
1932
|
+
};
|
|
1933
|
+
break;
|
|
1934
|
+
}
|
|
1935
|
+
if (isHttpResponseLike(hookResult)) {
|
|
1936
|
+
result = {
|
|
1937
|
+
ctx: currentCtx,
|
|
1938
|
+
response: normalizeResponse(hookResult),
|
|
1939
|
+
owner: "framework",
|
|
1940
|
+
};
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
if (hookResult?.ctx !== undefined) {
|
|
1944
|
+
currentCtx = contextRuntime.finalizeContext(
|
|
1945
|
+
hookResult.ctx,
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
if (hookResult?.response) {
|
|
1949
|
+
const response = normalizeHttpResponse(
|
|
1950
|
+
hookResult.response,
|
|
1951
|
+
);
|
|
1952
|
+
result = {
|
|
1953
|
+
ctx: currentCtx,
|
|
1954
|
+
response,
|
|
1955
|
+
owner: responseOwnerFor(response, "framework"),
|
|
1956
|
+
};
|
|
1957
|
+
break;
|
|
1819
1958
|
}
|
|
1820
1959
|
} catch (error) {
|
|
1821
1960
|
result = await resolveErrorResult(
|
|
@@ -1831,6 +1970,7 @@ function createRequestExecutor<
|
|
|
1831
1970
|
}
|
|
1832
1971
|
}
|
|
1833
1972
|
}
|
|
1973
|
+
stages.beforeHandleMs = performance.now() - beforeHandleStartedAt;
|
|
1834
1974
|
|
|
1835
1975
|
if (!result) {
|
|
1836
1976
|
// Hooks may have elevated the actor or resolved a tenant.
|
|
@@ -1845,7 +1985,9 @@ function createRequestExecutor<
|
|
|
1845
1985
|
result = {
|
|
1846
1986
|
ctx: currentCtx,
|
|
1847
1987
|
response: normalizeHttpResponse(
|
|
1848
|
-
await
|
|
1988
|
+
await timeStage("handlerMs", () =>
|
|
1989
|
+
userHandler({ ...baseArgs, ctx: currentCtx }),
|
|
1990
|
+
),
|
|
1849
1991
|
),
|
|
1850
1992
|
};
|
|
1851
1993
|
} catch (error) {
|
|
@@ -1864,12 +2006,21 @@ function createRequestExecutor<
|
|
|
1864
2006
|
}
|
|
1865
2007
|
}
|
|
1866
2008
|
|
|
2009
|
+
const sendStartedAt = performance.now();
|
|
1867
2010
|
result = await applyTransformHooks(result, true);
|
|
1868
2011
|
|
|
1869
2012
|
let finalResponse = normalizeHttpResponse(result.response);
|
|
1870
2013
|
let finalError = result.error;
|
|
1871
2014
|
let finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
2015
|
+
let responseValidation: ResponseFinalizerValidationState =
|
|
2016
|
+
"not-applicable";
|
|
1872
2017
|
if (
|
|
2018
|
+
finalOwner === "route" &&
|
|
2019
|
+
!isWebResponse(finalResponse) &&
|
|
2020
|
+
!(options.validateResponses ?? true)
|
|
2021
|
+
) {
|
|
2022
|
+
responseValidation = "disabled";
|
|
2023
|
+
} else if (
|
|
1873
2024
|
finalOwner === "route" &&
|
|
1874
2025
|
!isWebResponse(finalResponse) &&
|
|
1875
2026
|
(options.validateResponses ?? true)
|
|
@@ -1880,33 +2031,52 @@ function createRequestExecutor<
|
|
|
1880
2031
|
finalResponse,
|
|
1881
2032
|
target.responseValidationExemptStatus,
|
|
1882
2033
|
);
|
|
2034
|
+
result = {
|
|
2035
|
+
...result,
|
|
2036
|
+
response: finalResponse,
|
|
2037
|
+
};
|
|
2038
|
+
responseValidation = "validated";
|
|
1883
2039
|
} catch (error) {
|
|
1884
2040
|
if (error instanceof ResponseContractViolationError) {
|
|
1885
|
-
result =
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
);
|
|
2041
|
+
result = {
|
|
2042
|
+
ctx: result.ctx,
|
|
2043
|
+
response: toContractViolationResponse(error),
|
|
2044
|
+
error,
|
|
2045
|
+
owner: "framework",
|
|
2046
|
+
};
|
|
2047
|
+
finalResponse = normalizeHttpResponse(result.response);
|
|
2048
|
+
finalError = result.error;
|
|
2049
|
+
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
2050
|
+
result = await applyTransformHooks(result, true);
|
|
1894
2051
|
finalResponse = normalizeHttpResponse(result.response);
|
|
1895
2052
|
finalError = result.error;
|
|
1896
2053
|
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
2054
|
+
responseValidation = "not-applicable";
|
|
1897
2055
|
} else {
|
|
1898
2056
|
throw error;
|
|
1899
2057
|
}
|
|
1900
2058
|
}
|
|
1901
2059
|
}
|
|
2060
|
+
|
|
2061
|
+
result = await applyResponseFinalizerHooks(
|
|
2062
|
+
result,
|
|
2063
|
+
true,
|
|
2064
|
+
responseValidation,
|
|
2065
|
+
);
|
|
2066
|
+
finalResponse = normalizeHttpResponse(result.response);
|
|
2067
|
+
finalError = result.error;
|
|
2068
|
+
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
2069
|
+
|
|
1902
2070
|
if (!isWebResponse(finalResponse)) {
|
|
1903
2071
|
finalResponse = withFrameworkErrorOwnerHeader(
|
|
1904
2072
|
finalResponse,
|
|
1905
2073
|
finalOwner,
|
|
1906
2074
|
);
|
|
1907
2075
|
}
|
|
2076
|
+
stages.sendMs = performance.now() - sendStartedAt;
|
|
1908
2077
|
|
|
1909
2078
|
const durationMs = Date.now() - startedAt;
|
|
2079
|
+
const stageTimings = roundStageTimings(stages);
|
|
1910
2080
|
for (const hook of hooks) {
|
|
1911
2081
|
if (!hook.afterSend) continue;
|
|
1912
2082
|
try {
|
|
@@ -1921,6 +2091,7 @@ function createRequestExecutor<
|
|
|
1921
2091
|
response: responseForHooks(finalResponse),
|
|
1922
2092
|
error: finalError,
|
|
1923
2093
|
durationMs,
|
|
2094
|
+
stages: stageTimings,
|
|
1924
2095
|
});
|
|
1925
2096
|
} catch {
|
|
1926
2097
|
// Ignore after-response hook failures; they should never change the response.
|
|
@@ -1976,6 +2147,12 @@ function buildHandler<
|
|
|
1976
2147
|
routeHooks: readonly RouteHook<unknown, object>[] = [],
|
|
1977
2148
|
optionsOverrides?: {
|
|
1978
2149
|
skipRoutePreparation?: boolean;
|
|
2150
|
+
/**
|
|
2151
|
+
* Run the route through the full hook pipeline without contract
|
|
2152
|
+
* preparation: no query/path/header/body parsing or validation, and the
|
|
2153
|
+
* request body is left unconsumed for the handler.
|
|
2154
|
+
*/
|
|
2155
|
+
rawRoute?: boolean;
|
|
1979
2156
|
},
|
|
1980
2157
|
responseValidationExemptStatus?: number,
|
|
1981
2158
|
): (
|
|
@@ -2491,6 +2668,24 @@ export async function createServer<
|
|
|
2491
2668
|
const contract = resolveContract(contractLike);
|
|
2492
2669
|
return createBuilder(contract, true);
|
|
2493
2670
|
},
|
|
2671
|
+
rawRoute: (init) => ({
|
|
2672
|
+
handle: (fn) => {
|
|
2673
|
+
const built = buildHandler(
|
|
2674
|
+
options,
|
|
2675
|
+
finalPorts,
|
|
2676
|
+
contextRuntime,
|
|
2677
|
+
rawRouteContract(init),
|
|
2678
|
+
fn,
|
|
2679
|
+
hooks,
|
|
2680
|
+
[],
|
|
2681
|
+
{ rawRoute: true },
|
|
2682
|
+
);
|
|
2683
|
+
// The adapter owns routing for raw routes — the handler is mounted
|
|
2684
|
+
// at its own path — so path/method matching is skipped and
|
|
2685
|
+
// `init.path` stays identity for hooks and instrumentation.
|
|
2686
|
+
return (req) => built(req, {});
|
|
2687
|
+
},
|
|
2688
|
+
}),
|
|
2494
2689
|
createRequestContext: (req) => createRequestContext(req),
|
|
2495
2690
|
createServiceContext,
|
|
2496
2691
|
runServiceContext,
|
|
@@ -2500,6 +2695,32 @@ export async function createServer<
|
|
|
2500
2695
|
};
|
|
2501
2696
|
}
|
|
2502
2697
|
|
|
2698
|
+
function roundStageTimings(stages: RequestStageTimings): RequestStageTimings {
|
|
2699
|
+
const round = (value: number) => Math.round(value * 100) / 100;
|
|
2700
|
+
return {
|
|
2701
|
+
onRequestMs: round(stages.onRequestMs),
|
|
2702
|
+
parseMs: round(stages.parseMs),
|
|
2703
|
+
contextMs: round(stages.contextMs),
|
|
2704
|
+
beforeHandleMs: round(stages.beforeHandleMs),
|
|
2705
|
+
handlerMs: round(stages.handlerMs),
|
|
2706
|
+
sendMs: round(stages.sendMs),
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
function rawRouteContract(init: RawRouteInit): HttpContractConfig {
|
|
2711
|
+
return {
|
|
2712
|
+
kind: "http",
|
|
2713
|
+
name: init.name,
|
|
2714
|
+
method: init.method,
|
|
2715
|
+
path: init.path,
|
|
2716
|
+
pathParams: null,
|
|
2717
|
+
query: null,
|
|
2718
|
+
body: null,
|
|
2719
|
+
responses: {},
|
|
2720
|
+
metadata: init.metadata ?? {},
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2503
2724
|
/**
|
|
2504
2725
|
* Define and flatten route registrations with strong type inference.
|
|
2505
2726
|
*
|