@beignet/core
Version:
Core framework primitives for Beignet
1,250 lines (1,191 loc) • 38.8 kB
text/typescript
import type { HttpContractConfig } from "../contracts/index.js";
import {
httpErrors,
isAppError,
toErrorResponseBody,
} from "../errors/index.js";
import {
IdempotencyConflictError,
IdempotencyInProgressError,
} from "../idempotency/index.js";
import {
type MemoInstrumentationEvent,
runWithMemoScope,
} from "../memo/index.js";
import type { AnyPorts } from "../ports/index.js";
import {
AuthUnauthorizedError,
EntitlementRequiredError,
GateAuthorizationError,
TenantRequiredError,
} from "../ports/index.js";
import {
createProviderInstrumentation,
type ProviderInstrumentationTarget,
resolveProviderInstrumentationPort,
} from "../providers/index.js";
import {
parseTraceparent,
resolveTracingPort,
runWithTracing,
type TraceContextInput,
} from "../tracing/index.js";
import type { ContextSeed } from "./context.js";
import type {
Handler,
HandlerArgs,
HttpRequestLike,
HttpResponse,
RequestStageTimings,
RouteHook,
ServerCaughtErrorHook,
ServerHook,
ServerUnhandledErrorMapper,
} from "./http.js";
import type { ServerInstrumentationOptions } from "./instrumentation.js";
import {
getResponseFinalizerHook,
type ResponseFinalizerValidationState,
} from "./internal-hooks.js";
import {
readContextActor,
readContextTenant,
setActiveRequestIdentity,
} from "./request-context.js";
import {
prepareRequestInputs,
type RequestBodyOptions,
requestBodyLimit,
requestHeadersToRecord,
} from "./request-preparation.js";
import {
defaultErrorResponse,
errorResponse,
finalizeResponse,
isHttpResponseLike,
isWebResponse,
mergeNativeResponseHeaders,
normalizeHttpResponse,
normalizeResponse,
ResponseContractViolationError,
type ResponseOwner,
responseForHooks,
responseOwnerFor,
toContractViolationResponse,
withContractLifecycleHeaders,
withFrameworkErrorOwnerHeader,
} from "./response-finalization.js";
import {
type CompiledPath,
compilePath,
decodeMatchedParams,
PathDecodeError,
} from "./route-matching.js";
import type { TrustedRequestInfo } from "./trusted-proxy.js";
import { InvalidRequestUrlError } from "./trusted-proxy-internal.js";
import { UseCaseRouteInputValidationError } from "./use-case-route.js";
function withoutHeadResponseBody(
response: HttpResponse,
requestMethod: string,
): HttpResponse {
if (requestMethod.toUpperCase() !== "HEAD") return response;
if (isWebResponse(response)) {
if (response.body === null) return response;
void response.body.cancel().catch(() => {});
return new Response(null, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
}
const { body: _body, ...headResponse } = response;
return headResponse;
}
type RequestExecutorOptions<Ctx> = {
instrumentation?: ServerInstrumentationOptions<Ctx> | false;
validateResponses?: boolean;
requestBody?: RequestBodyOptions;
onCaughtError?: ServerCaughtErrorHook<Ctx>;
mapUnhandledError?: ServerUnhandledErrorMapper<Ctx>;
};
type ExecutionResult<Ctx> = {
ctx?: Ctx;
response: HttpResponse;
error?: unknown;
owner?: ResponseOwner;
};
type ExecutionTarget<Ctx, C extends HttpContractConfig> = {
contract: C;
/**
* Compiled route pattern. Only required when the executor is invoked
* without pre-matched params.
*/
compiled?: CompiledPath;
handler: Handler<Ctx, C>;
/**
* Success status whose response schema validation is skipped because the
* use case bound to this route already validated its output against the
* same schema object.
*/
responseValidationExemptStatus?: number;
};
function copyTrustedRequestInfo(
requestInfo: TrustedRequestInfo,
): TrustedRequestInfo;
function copyTrustedRequestInfo(requestInfo: undefined): undefined;
function copyTrustedRequestInfo(
requestInfo: TrustedRequestInfo | undefined,
): TrustedRequestInfo | undefined;
function copyTrustedRequestInfo(
requestInfo: TrustedRequestInfo | undefined,
): TrustedRequestInfo | undefined {
if (!requestInfo) return undefined;
return {
...requestInfo,
url: new URL(requestInfo.url),
};
}
/**
* Build the per-request execution pipeline once.
*
* The returned executor takes the contract, compiled pattern, and user handler
* per invocation so fallback responses (404/405) can reuse a single pipeline
* across requests instead of rebuilding it per unmatched request.
*/
export function createRequestExecutor<
Ctx,
FinalPorts extends AnyPorts,
C extends HttpContractConfig,
>(
options: RequestExecutorOptions<Ctx>,
finalPorts: FinalPorts,
contextRuntime: {
createRequestContext: (
req: HttpRequestLike,
contract: HttpContractConfig,
requestInfo: TrustedRequestInfo,
) => Promise<Ctx>;
finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
resolveRequestInfo: (req: HttpRequestLike) => TrustedRequestInfo;
},
hooks: ServerHook<Ctx, FinalPorts>[],
routeHooks: readonly RouteHook<unknown, object>[] = [],
optionsOverrides?: {
skipRoutePreparation?: boolean;
/**
* Run the route through the full hook pipeline without contract
* preparation: no query/path/header/body parsing or validation, and the
* request body is left unconsumed for the handler.
*/
rawRoute?: boolean;
},
): (
target: ExecutionTarget<Ctx, C>,
req: HttpRequestLike,
preMatchedParams?: Record<string, string>,
) => Promise<HttpResponse> {
const warnedNativeReplacementHooks = new WeakSet<object>();
const maxRequestBodyBytes = requestBodyLimit(options.requestBody);
const executeRequest = async (
target: ExecutionTarget<Ctx, C>,
req: HttpRequestLike,
preMatchedParams?: Record<string, string>,
) => {
const { contract, handler: userHandler } = target;
let requestInfo: TrustedRequestInfo | undefined;
let baseCtx: Ctx | undefined;
let pathValue: HandlerArgs<Ctx, C>["path"] | undefined;
let queryValue: HandlerArgs<Ctx, C>["query"] | undefined;
let headersValue: HandlerArgs<Ctx, C>["headers"] | undefined;
let bodyValue: HandlerArgs<Ctx, C>["body"] | undefined;
const startedAt = Date.now();
// Stage timings accumulate because retries re-enter the send phase and
// beforeHandle spans the route-hook and server-hook loops.
const stages: RequestStageTimings = {
onRequestMs: 0,
parseMs: 0,
contextMs: 0,
beforeHandleMs: 0,
handlerMs: 0,
sendMs: 0,
};
const timeStage = async <T>(
stage: keyof RequestStageTimings,
run: () => Promise<T> | T,
): Promise<T> => {
const stageStartedAt = performance.now();
try {
return await run();
} finally {
stages[stage] += performance.now() - stageStartedAt;
}
};
const resolveErrorResult = async (
error: unknown,
ctx?: Ctx,
path?: HandlerArgs<Ctx, C>["path"],
query?: HandlerArgs<Ctx, C>["query"],
headers?: HandlerArgs<Ctx, C>["headers"],
body?: HandlerArgs<Ctx, C>["body"],
resultOptions?: {
owner?: ResponseOwner;
},
): Promise<ExecutionResult<Ctx>> => {
let currentError = error;
const notifyCaughtError = async (caught: unknown) => {
const args = {
err: caught,
req,
requestInfo: copyTrustedRequestInfo(requestInfo),
ctx,
contract,
path,
query,
headers,
body,
};
for (const hook of hooks) {
if (!hook.onCaughtError) continue;
try {
await hook.onCaughtError(args);
} catch {
// Observers must not change response behavior.
}
}
if (options.onCaughtError) {
try {
await options.onCaughtError(args);
} catch {
// Observers must not change response behavior.
}
}
};
await notifyCaughtError(currentError);
if (currentError instanceof ResponseContractViolationError) {
return {
ctx,
response: toContractViolationResponse(currentError),
error: currentError,
owner: "framework",
};
}
if (currentError instanceof UseCaseRouteInputValidationError) {
return {
ctx,
response: errorResponse(
500,
currentError.code,
currentError.message,
{
contractName: currentError.contractName,
useCaseName: currentError.useCaseName,
location: "useCaseInput",
},
),
error: currentError,
owner: "framework",
};
}
if (currentError instanceof InvalidRequestUrlError) {
return {
ctx,
response: errorResponse(
400,
"INVALID_REQUEST_URL",
"Malformed request URL",
),
error: currentError,
owner: "framework",
};
}
if (isAppError(currentError)) {
return {
ctx,
response: {
status: currentError.status,
...(currentError.headers
? { headers: { ...currentError.headers } }
: {}),
body: toErrorResponseBody(currentError),
},
error: currentError,
owner: resultOptions?.owner ?? "route",
};
}
if (currentError instanceof AuthUnauthorizedError) {
return {
ctx,
response: errorResponse(401, currentError.code, currentError.message),
error: currentError,
owner: "framework",
};
}
if (currentError instanceof TenantRequiredError) {
return {
ctx,
response: errorResponse(
currentError.status,
currentError.code,
currentError.message,
),
error: currentError,
owner: "framework",
};
}
if (currentError instanceof IdempotencyConflictError) {
return {
ctx,
response: errorResponse(
httpErrors.IdempotencyConflict.status,
httpErrors.IdempotencyConflict.code,
currentError.message,
{
namespace: currentError.namespace,
key: currentError.key,
},
),
error: currentError,
owner: "framework",
};
}
if (currentError instanceof IdempotencyInProgressError) {
return {
ctx,
response: errorResponse(
httpErrors.IdempotencyInProgress.status,
httpErrors.IdempotencyInProgress.code,
currentError.message,
{
namespace: currentError.namespace,
key: currentError.key,
},
),
error: currentError,
owner: "framework",
};
}
if (currentError instanceof GateAuthorizationError) {
return {
ctx,
response: errorResponse(
currentError.status,
currentError.code,
currentError.message,
currentError.details,
),
error: currentError,
owner: "framework",
};
}
if (currentError instanceof EntitlementRequiredError) {
return {
ctx,
response: errorResponse(
currentError.status,
currentError.code,
currentError.message,
currentError.details,
),
error: currentError,
owner: "framework",
};
}
for (const hook of hooks) {
if (!hook.mapUnhandledError) continue;
try {
const handled = await hook.mapUnhandledError({
err: currentError,
req,
requestInfo: copyTrustedRequestInfo(requestInfo),
ctx,
contract,
path,
query,
headers,
body,
});
if (handled) {
const response = normalizeHttpResponse(handled);
return {
ctx,
response,
error: currentError,
owner: responseOwnerFor(response, "framework"),
};
}
} catch (hookError) {
currentError = hookError;
await notifyCaughtError(currentError);
}
}
if (options.mapUnhandledError) {
try {
const handled = await options.mapUnhandledError({
err: currentError,
req,
requestInfo: copyTrustedRequestInfo(requestInfo),
ctx,
contract,
path,
query,
headers,
body,
});
if (handled) {
const response = normalizeHttpResponse(handled);
return {
ctx,
response,
error: currentError,
owner: responseOwnerFor(response, "framework"),
};
}
} catch (hookError) {
currentError = hookError;
await notifyCaughtError(currentError);
}
}
return {
ctx,
response: defaultErrorResponse(currentError, ctx),
error: currentError,
owner: "framework",
};
};
try {
const resolvedRequestInfo = contextRuntime.resolveRequestInfo(req);
requestInfo = resolvedRequestInfo;
const url = new URL(req.url);
let matchedParams: Record<string, string>;
if (preMatchedParams) {
matchedParams = preMatchedParams;
} else {
const compiled = target.compiled;
const match = compiled ? compiled.pattern.exec(url.pathname) : null;
const contractMethod = contract.method.toUpperCase();
const requestMethod = req.method.toUpperCase();
const methodMatches =
contractMethod === requestMethod ||
(contractMethod === "GET" && requestMethod === "HEAD");
if (!compiled || !match || !methodMatches) {
return errorResponse(404, "NOT_FOUND", "Not found");
}
try {
matchedParams = decodeMatchedParams(compiled.keys, match);
} catch (error) {
if (error instanceof PathDecodeError) {
return errorResponse(400, "INVALID_PATH", "Malformed URL path");
}
throw error;
}
}
const rawHeaders = requestHeadersToRecord(req.headers);
const runNativeBeforeSend = async (
initialResult: ExecutionResult<Ctx>,
nativeResponse: Response,
): Promise<Response> => {
const originalView = responseForHooks(nativeResponse);
const originalHeaders = originalView.headers ?? {};
let transformed = originalView;
for (const hook of hooks) {
if (!hook.beforeSend) continue;
const nextResponse = await hook.beforeSend({
req,
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
ctx: initialResult.ctx,
contract,
path: pathValue,
query: queryValue,
headers: headersValue,
body: bodyValue,
response: transformed,
error: initialResult.error,
native: true,
});
if (nextResponse) {
if (
(nextResponse.status !== nativeResponse.status ||
nextResponse.body !== undefined) &&
!warnedNativeReplacementHooks.has(hook) &&
process.env.NODE_ENV !== "production"
) {
warnedNativeReplacementHooks.add(hook);
console.warn(
`[beignet] beforeSend hook "${hook.name ?? "(anonymous)"}" returned a replacement status or body for a native Response on ${contract.method} ${contract.path}. Native responses are headers-only in beforeSend; status and body changes are ignored.`,
);
}
transformed = {
status: nativeResponse.status,
headers: nextResponse.headers,
};
}
}
return mergeNativeResponseHeaders(
nativeResponse,
originalHeaders,
transformed.headers ?? {},
);
};
const applyTransformHooks = async (
initialResult: ExecutionResult<Ctx>,
allowRetry: boolean,
): Promise<ExecutionResult<Ctx>> => {
try {
if (isWebResponse(initialResult.response)) {
return {
...initialResult,
response: await runNativeBeforeSend(
initialResult,
initialResult.response,
),
};
}
let transformed = normalizeResponse(initialResult.response);
for (const hook of hooks) {
if (!hook.beforeSend) continue;
const nextResponse = await hook.beforeSend({
req,
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
ctx: initialResult.ctx,
contract,
path: pathValue,
query: queryValue,
headers: headersValue,
body: bodyValue,
response: transformed,
error: initialResult.error,
});
if (nextResponse) {
transformed = normalizeResponse(nextResponse);
}
}
return {
...initialResult,
response: transformed,
};
} catch (error) {
const mapped = await resolveErrorResult(
error,
initialResult.ctx,
pathValue,
queryValue,
headersValue,
bodyValue,
{ owner: "framework" },
);
if (!allowRetry) {
return mapped;
}
return applyTransformHooks(mapped, false);
}
};
const applyResponseFinalizerHooks = async (
initialResult: ExecutionResult<Ctx>,
allowRetry: boolean,
responseValidation: ResponseFinalizerValidationState,
): Promise<ExecutionResult<Ctx>> => {
const response = normalizeHttpResponse(initialResult.response);
const native = isWebResponse(response);
const owner = responseOwnerFor(response, initialResult.owner);
try {
for (const hook of hooks) {
const finalizer = getResponseFinalizerHook<Ctx>(hook);
if (!finalizer) continue;
await finalizer({
req,
ctx: initialResult.ctx,
contract,
path: pathValue,
query: queryValue,
headers: headersValue,
body: bodyValue,
response: responseForHooks(response),
error: initialResult.error,
native: native ? true : undefined,
owner,
responseValidation,
});
}
return {
...initialResult,
response,
};
} catch (error) {
const mapped = await resolveErrorResult(
error,
initialResult.ctx,
pathValue,
queryValue,
headersValue,
bodyValue,
{ owner: "framework" },
);
if (!allowRetry) {
return mapped;
}
const transformed = await applyTransformHooks(mapped, true);
return applyResponseFinalizerHooks(
transformed,
false,
"not-applicable",
);
}
};
let result: ExecutionResult<Ctx> | undefined;
const onRequestStartedAt = performance.now();
for (const hook of hooks) {
if (!hook.onRequest) continue;
try {
const hookResult = await hook.onRequest({
req,
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
ports: finalPorts,
contract,
params: matchedParams,
});
if (hookResult) {
const response = normalizeHttpResponse(hookResult);
result = {
response,
owner: responseOwnerFor(response, "framework"),
};
break;
}
} catch (error) {
result = await resolveErrorResult(
error,
undefined,
undefined,
undefined,
undefined,
undefined,
{ owner: "framework" },
);
break;
}
}
stages.onRequestMs = performance.now() - onRequestStartedAt;
if (!result) {
if (optionsOverrides?.skipRoutePreparation) {
let createdCtx!: Ctx;
try {
createdCtx = await timeStage("contextMs", () =>
contextRuntime.createRequestContext(
req,
contract,
resolvedRequestInfo,
),
);
baseCtx = createdCtx;
} catch (error) {
result = await resolveErrorResult(
error,
undefined,
undefined,
undefined,
undefined,
undefined,
{ owner: "framework" },
);
}
if (!result) {
try {
result = {
ctx: createdCtx,
response: normalizeHttpResponse(
await timeStage("handlerMs", () =>
userHandler({
req,
ctx: createdCtx,
contract,
path: {} as HandlerArgs<Ctx, C>["path"],
query: {} as HandlerArgs<Ctx, C>["query"],
headers: rawHeaders as HandlerArgs<Ctx, C>["headers"],
body: undefined as HandlerArgs<Ctx, C>["body"],
}),
),
),
owner: "framework",
};
} catch (error) {
result = await resolveErrorResult(
error,
createdCtx,
undefined,
undefined,
undefined,
undefined,
{ owner: "framework" },
);
}
}
} else {
const parseStartedAt = performance.now();
let path = undefined as HandlerArgs<Ctx, C>["path"] | undefined;
let query = undefined as HandlerArgs<Ctx, C>["query"] | undefined;
let headers = undefined as HandlerArgs<Ctx, C>["headers"] | undefined;
let body = undefined as HandlerArgs<Ctx, C>["body"] | undefined;
const prepared = await prepareRequestInputs({
contract,
req,
url,
rawHeaders,
matchedParams,
maxRequestBodyBytes,
rawRoute: optionsOverrides?.rawRoute,
});
if (!prepared.ok) {
result = {
response: prepared.response,
owner: "framework",
};
} else {
path = prepared.inputs.path as HandlerArgs<Ctx, C>["path"];
query = prepared.inputs.query as HandlerArgs<Ctx, C>["query"];
headers = prepared.inputs.headers as HandlerArgs<Ctx, C>["headers"];
body = prepared.inputs.body as HandlerArgs<Ctx, C>["body"];
}
stages.parseMs = performance.now() - parseStartedAt;
if (!result) {
pathValue = path;
queryValue = query;
headersValue = headers;
bodyValue = body;
let createdCtx!: Ctx;
try {
createdCtx = await timeStage("contextMs", () =>
contextRuntime.createRequestContext(
req,
contract,
resolvedRequestInfo,
),
);
baseCtx = createdCtx;
} catch (error) {
result = await resolveErrorResult(
error,
undefined,
pathValue,
queryValue,
headersValue,
bodyValue,
{ owner: "framework" },
);
}
if (!result) {
const baseArgs: HandlerArgs<Ctx, C> = {
req,
ctx: createdCtx,
contract,
path: path as HandlerArgs<Ctx, C>["path"],
query: query as HandlerArgs<Ctx, C>["query"],
headers: headers as HandlerArgs<Ctx, C>["headers"],
body: body as HandlerArgs<Ctx, C>["body"],
};
let currentCtx = createdCtx;
const beforeHandleStartedAt = performance.now();
for (const hook of routeHooks) {
try {
const additions = await hook.resolve({
req,
ctx: currentCtx,
contract,
path,
query,
headers,
body,
});
if (additions && typeof additions === "object") {
currentCtx = contextRuntime.finalizeContext({
...(currentCtx as object),
...additions,
} as Ctx);
}
} catch (error) {
result = await resolveErrorResult(
error,
currentCtx,
pathValue,
queryValue,
headersValue,
bodyValue,
{ owner: "framework" },
);
break;
}
}
if (!result) {
for (const hook of hooks) {
if (!hook.beforeHandle) continue;
try {
const hookResult = await hook.beforeHandle({
req,
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
ctx: currentCtx,
contract,
path,
query,
headers,
body,
});
if (isWebResponse(hookResult)) {
result = {
ctx: currentCtx,
response: hookResult,
owner: "transport",
};
break;
}
if (isHttpResponseLike(hookResult)) {
result = {
ctx: currentCtx,
response: normalizeResponse(hookResult),
owner: "framework",
};
break;
}
if (hookResult?.ctx !== undefined) {
currentCtx = contextRuntime.finalizeContext(
hookResult.ctx,
);
}
if (hookResult?.response) {
const response = normalizeHttpResponse(
hookResult.response,
);
result = {
ctx: currentCtx,
response,
owner: responseOwnerFor(response, "framework"),
};
break;
}
} catch (error) {
result = await resolveErrorResult(
error,
currentCtx,
pathValue,
queryValue,
headersValue,
bodyValue,
{ owner: "framework" },
);
break;
}
}
}
stages.beforeHandleMs = performance.now() - beforeHandleStartedAt;
if (!result) {
// Hooks may have elevated the actor or resolved a tenant.
// Refresh the ambient request context so record-time
// consumers such as createAmbientAuditLog see the finalized
// identity.
setActiveRequestIdentity({
actor: readContextActor(currentCtx),
tenant: readContextTenant(currentCtx),
});
try {
result = {
ctx: currentCtx,
response: normalizeHttpResponse(
await timeStage("handlerMs", () =>
userHandler({ ...baseArgs, ctx: currentCtx }),
),
),
};
} catch (error) {
result = await resolveErrorResult(
error,
currentCtx,
pathValue,
queryValue,
headersValue,
bodyValue,
);
}
}
}
}
}
}
const sendStartedAt = performance.now();
result = await applyTransformHooks(result, true);
let finalResponse = normalizeHttpResponse(result.response);
let finalError = result.error;
let finalOwner = responseOwnerFor(finalResponse, result.owner);
let responseValidation: ResponseFinalizerValidationState =
"not-applicable";
if (finalOwner === "route" && !isWebResponse(finalResponse)) {
const validateContract = options.validateResponses ?? true;
try {
finalResponse = await finalizeResponse(
contract,
finalResponse,
target.responseValidationExemptStatus,
{ validateContract },
);
result = {
...result,
response: finalResponse,
};
responseValidation = validateContract ? "validated" : "disabled";
} catch (error) {
if (error instanceof ResponseContractViolationError) {
result = {
ctx: result.ctx,
response: toContractViolationResponse(error),
error,
owner: "framework",
};
} else {
result = await resolveErrorResult(
error,
result.ctx,
pathValue,
queryValue,
headersValue,
bodyValue,
{ owner: "framework" },
);
}
finalResponse = normalizeHttpResponse(result.response);
finalError = result.error;
finalOwner = responseOwnerFor(finalResponse, result.owner);
result = await applyTransformHooks(result, true);
finalResponse = normalizeHttpResponse(result.response);
finalError = result.error;
finalOwner = responseOwnerFor(finalResponse, result.owner);
responseValidation = "not-applicable";
}
}
result = await applyResponseFinalizerHooks(
result,
true,
responseValidation,
);
finalResponse = normalizeHttpResponse(result.response);
finalError = result.error;
finalOwner = responseOwnerFor(finalResponse, result.owner);
if (!isWebResponse(finalResponse)) {
finalResponse = withFrameworkErrorOwnerHeader(
finalResponse,
finalOwner,
);
}
finalResponse = withContractLifecycleHeaders(finalResponse, contract);
finalResponse = withoutHeadResponseBody(finalResponse, req.method);
stages.sendMs = performance.now() - sendStartedAt;
const durationMs = Date.now() - startedAt;
const stageTimings = roundStageTimings(stages);
for (const hook of hooks) {
if (!hook.afterSend) continue;
try {
await hook.afterSend({
req,
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
ctx: result.ctx,
contract,
path: pathValue,
query: queryValue,
headers: headersValue,
body: bodyValue,
response: responseForHooks(finalResponse),
error: finalError,
durationMs,
stages: stageTimings,
});
} catch {
// Ignore after-response hook failures; they should never change the response.
}
}
return finalResponse;
} catch (error) {
const result = await resolveErrorResult(
error,
baseCtx,
pathValue,
queryValue,
headersValue,
bodyValue,
{
owner: "framework",
},
);
const response = withoutHeadResponseBody(
withContractLifecycleHeaders(
normalizeHttpResponse(result.response),
contract,
),
req.method,
);
if (isWebResponse(response)) {
return response;
}
return withFrameworkErrorOwnerHeader(
response,
responseOwnerFor(response, result.owner),
);
}
};
return async (
target: ExecutionTarget<Ctx, C>,
req: HttpRequestLike,
preMatchedParams?: Record<string, string>,
) => {
const tracing = resolveTracingPort(finalPorts);
const instrumentationOptions =
options.instrumentation === false ? undefined : options.instrumentation;
const pathname = (() => {
try {
return new URL(req.url).pathname;
} catch {
return req.url;
}
})();
const ignored = (
instrumentationOptions?.ignorePaths ?? ["/api/devtools"]
).some((prefix) => {
const normalized = prefix.replace(/\/+$/, "");
return pathname === normalized || pathname.startsWith(`${normalized}/`);
});
if (!tracing || ignored) {
return executeRequest(target, req, preMatchedParams);
}
const traceContextHeader =
instrumentationOptions?.traceContextHeader ?? "traceparent";
const active = tracing.current();
const parsedTraceparent =
traceContextHeader === false
? undefined
: parseTraceparent(req.headers.get(traceContextHeader));
const parent: TraceContextInput | undefined = active
? undefined
: !parsedTraceparent
? undefined
: {
traceparent: parsedTraceparent.traceparent,
tracestate: req.headers.get("tracestate") ?? undefined,
};
const traceAttributes = {
"beignet.contract.name": target.contract.name,
"http.request.method": req.method.toUpperCase(),
"http.route": target.contract.path,
} as const;
return await runWithTracing(
tracing,
{
name: `beignet.request ${target.contract.name}`,
type: "request",
kind: active ? "internal" : "server",
parent,
attributes: traceAttributes,
metricAttributes: traceAttributes,
},
async (span) => {
const response = await executeRequest(target, req, preMatchedParams);
span?.setAttribute("http.response.status_code", response.status);
if (response.status >= 500) span?.setStatus("error");
return response;
},
);
};
}
/**
* Build a memo instrumentation recorder from the app ports, or undefined when
* no instrumentation sink is wired.
*
* Resolved per execution rather than at route-build time because providers
* contribute `ports.instrumentation`/`ports.devtools` during setup, after
* routes are registered.
*/
export function createMemoScopeRecorder(
ports: AnyPorts,
): ((event: MemoInstrumentationEvent) => void) | undefined {
const target = ports as ProviderInstrumentationTarget;
if (!resolveProviderInstrumentationPort(target)) return undefined;
const instrumentation = createProviderInstrumentation(target, {
providerName: "memo",
watcher: "memo",
});
return (event) => {
instrumentation.custom({
name: `memo.${event.kind}`,
label: event.kind === "hit" ? "Memo hit" : "Memo fill",
summary: `Memo ${event.kind} for ${event.memo}`,
details: {
memo: event.memo,
key: event.key,
...(event.durationMs !== undefined
? { durationMs: event.durationMs }
: {}),
...(event.failed ? { failed: true } : {}),
},
});
};
}
export function buildHandler<
Ctx,
FinalPorts extends AnyPorts,
C extends HttpContractConfig,
>(
options: RequestExecutorOptions<Ctx>,
finalPorts: FinalPorts,
contextRuntime: {
createRequestContext: (
req: HttpRequestLike,
contract: HttpContractConfig,
requestInfo: TrustedRequestInfo,
) => Promise<Ctx>;
finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
resolveRequestInfo: (req: HttpRequestLike) => TrustedRequestInfo;
},
contract: C,
userHandler: Handler<Ctx, C>,
hooks: ServerHook<Ctx, FinalPorts>[],
routeHooks: readonly RouteHook<unknown, object>[] = [],
optionsOverrides?: {
skipRoutePreparation?: boolean;
/**
* Run the route through the full hook pipeline without contract
* preparation: no query/path/header/body parsing or validation, and the
* request body is left unconsumed for the handler.
*/
rawRoute?: boolean;
},
responseValidationExemptStatus?: number,
): (
req: HttpRequestLike,
preMatchedParams?: Record<string, string>,
) => Promise<HttpResponse> {
const execute = createRequestExecutor<Ctx, FinalPorts, C>(
options,
finalPorts,
contextRuntime,
hooks,
routeHooks,
optionsOverrides,
);
const executionTarget: ExecutionTarget<Ctx, C> = {
contract,
compiled: compilePath(contract.path),
handler: userHandler,
responseValidationExemptStatus,
};
// Every HTTP execution runs inside a fresh memo scope so createMemo(...)
// wrappers dedupe lookups for exactly one request.
return (req, preMatchedParams) =>
runWithMemoScope({ record: createMemoScopeRecorder(finalPorts) }, () =>
execute(executionTarget, req, preMatchedParams),
);
}
function roundStageTimings(stages: RequestStageTimings): RequestStageTimings {
const round = (value: number) => Math.round(value * 100) / 100;
return {
onRequestMs: round(stages.onRequestMs),
parseMs: round(stages.parseMs),
contextMs: round(stages.contextMs),
beforeHandleMs: round(stages.beforeHandleMs),
handlerMs: round(stages.handlerMs),
sendMs: round(stages.sendMs),
};
}