fix(rpc): close the service-collision fail-open and the fix-wave gaps
Critical: - router: fail loudly (WRN-SERVICE-COLLISION) when two app/services files scan to the same service name, instead of silently letting directory-walk order pick a winner. Important: - server.ts: wrap a throwing input schema so its raw message cannot escape invoke(); returns RPC_INVALID and logs server-side instead. - client.ts: race timeoutMs against transport.call so a stalled transport cannot hang the caller; rejects with a ServiceError(RPC_TRANSPORT). - client.ts: the proxy returns undefined for undeclared properties (incl. then/catch/finally) instead of a function that throws, closing the await-client thenable trap. - gateway.ts / rpc-dispatch.ts: import RPC_PATH_PREFIX / RPC_INTERNAL_HEADER from @wrnexus/rpc instead of hardcoding local copies. - gateway.test.ts: cover the RPC-prefix edge block and internal-header stripping across casing variants. - http.test.ts / client.test.ts: cover anonymous-call header omission, the internal marker, the retryable-status sweep, network/malformed/HTML failures, AbortSignal propagation, the timeout path, and timer cleanup. Minor: - transport.ts: Object.hasOwn for handler lookup; note the entry-only abort check. - client.ts: wrap a missing/invalid WRNEXUS_RPC_SECRET as a ServiceError (RPC_IDENTITY) instead of a bare Error. - rpc/package.json: drop the unused @wrnexus/authz dependency. - server.ts: implement() now throws at construction time if a declared procedure has no own handler. Verified: reverting the service-collision check and the client timeout race each make their new test fail, then restore green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,16 @@ export function implement<Procedures extends AnyProcedures>(
|
||||
handlers: ServiceHandlers<Procedures>,
|
||||
options: ImplementOptions,
|
||||
): ServiceImplementation<Procedures> {
|
||||
// A declared procedure with no own handler would otherwise only surface at
|
||||
// invoke time as RPC_UNKNOWN — a silent, permanent 404. Catch it now.
|
||||
for (const procedureName of Object.keys(contract.procedures)) {
|
||||
if (!Object.hasOwn(handlers, procedureName)) {
|
||||
throw new Error(
|
||||
`WRN-RPC-HANDLER: service "${contract.name}" declares procedure "${procedureName}" ` +
|
||||
`but implement() was not given a handler for it.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
contract,
|
||||
async invoke(procedureName, payload, identity) {
|
||||
@@ -69,7 +79,17 @@ export function implement<Procedures extends AnyProcedures>(
|
||||
|
||||
let input: unknown = payload;
|
||||
if (definition.input) {
|
||||
const parsed = definition.input.parse(payload as Record<string, unknown>);
|
||||
// InputSchema is structural: any custom or wrapped schema may throw
|
||||
// instead of returning { ok: false }. A throw must not escape invoke()
|
||||
// with its raw message — that text can carry internals — so it is
|
||||
// caught the same way the permission check above is.
|
||||
let parsed: { ok: boolean; value?: unknown };
|
||||
try {
|
||||
parsed = definition.input.parse(payload as Record<string, unknown>);
|
||||
} catch (error) {
|
||||
console.error(`[wrnexus] RPC input schema threw for ${String(procedureName)}`, error);
|
||||
return failure(RPC_ERROR_CODES.invalid, "Invalid input");
|
||||
}
|
||||
if (!parsed.ok) return failure(RPC_ERROR_CODES.invalid, "Invalid input");
|
||||
input = parsed.value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user