Skip to content

Commit 7162a2b

Browse files
authored
feat(devframe): forward onFunctionError/onGeneralError to startHttpAndWs (#84)
1 parent 38cab6b commit 7162a2b

3 files changed

Lines changed: 125 additions & 5 deletions

File tree

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@
1414
},
1515
"bugs": "https://github.com/devframes/devframe/issues",
1616
"scripts": {
17-
"build": "turbo run build",
17+
"build": "turbo run build --concurrency=3",
1818
"watch": "pnpm -r run watch",
1919
"docs": "pnpm -C docs run docs",
2020
"docs:build": "pnpm -C docs run docs:build",
2121
"docs:serve": "pnpm -C docs run docs:serve",
2222
"storybook": "pnpm -r --parallel --if-present run storybook",
2323
"storybook:build": "pnpm --filter @devframes/storybook run storybook:build",
2424
"lint": "eslint --cache",
25-
"test": "turbo run build && vitest",
26-
"test:e2e": "turbo run build && playwright test",
27-
"test:e2e:ui": "turbo run build && playwright test --ui",
25+
"test": "pnpm run build && vitest",
26+
"test:e2e": "pnpm run build && playwright test",
27+
"test:e2e:ui": "pnpm run build && playwright test --ui",
2828
"test:ecosystem": "tsx scripts/ecosystem-ci.ts",
2929
"release": "bumpp -r",
3030
"typecheck": "pnpm run verify:typecheck-coverage && turbo run typecheck",
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { DevframeHost, DevframeNodeContext, DevframeRpcClientFunctions, DevframeRpcServerFunctions } from 'devframe/types'
2+
import { mkdtempSync } from 'node:fs'
3+
import { tmpdir } from 'node:os'
4+
import { join } from 'node:path'
5+
import { createRpcClient } from 'devframe/rpc/client'
6+
import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client'
7+
import { getPort } from 'get-port-please'
8+
import { describe, expect, it, vi } from 'vitest'
9+
import { WebSocket } from 'ws'
10+
import { createHostContext } from '../context'
11+
import { startHttpAndWs } from '../server'
12+
13+
function makeHost(storageDir: string): DevframeHost {
14+
return {
15+
mountStatic: () => {},
16+
resolveOrigin: () => 'http://localhost',
17+
getStorageDir: () => storageDir,
18+
}
19+
}
20+
21+
async function createTestContext(): Promise<DevframeNodeContext> {
22+
const storageDir = mkdtempSync(join(tmpdir(), 'devframe-server-'))
23+
return createHostContext({ cwd: storageDir, mode: 'dev', host: makeHost(storageDir) })
24+
}
25+
26+
function connectClient(host: string, port: number) {
27+
return createRpcClient<DevframeRpcServerFunctions, DevframeRpcClientFunctions>(
28+
{} as DevframeRpcClientFunctions,
29+
{ channel: createWsRpcChannel({ url: `ws://${host}:${port}` }) },
30+
)
31+
}
32+
33+
describe('startHttpAndWs rpcOptions passthrough', () => {
34+
it('forwards a thrown handler error to rpcOptions.onFunctionError without swallowing the response', async () => {
35+
const context = await createTestContext()
36+
context.rpc.register({
37+
name: 'test:boom',
38+
type: 'action',
39+
handler: () => {
40+
throw new Error('kaboom')
41+
},
42+
})
43+
44+
const onFunctionError = vi.fn()
45+
const host = '127.0.0.1'
46+
const port = await getPort({ port: 0, host })
47+
const server = await startHttpAndWs({
48+
context,
49+
host,
50+
port,
51+
auth: false,
52+
rpcOptions: { onFunctionError },
53+
})
54+
55+
try {
56+
const client = connectClient(host, port)
57+
await expect(client.$call('test:boom' as any)).rejects.toThrow('kaboom')
58+
59+
expect(onFunctionError).toHaveBeenCalledTimes(1)
60+
const [error, name] = onFunctionError.mock.calls[0]!
61+
expect(name).toBe('test:boom')
62+
expect((error as Error).message).toBe('kaboom')
63+
client.$close()
64+
}
65+
finally {
66+
await server.close()
67+
}
68+
})
69+
70+
it('forwards a deserialize failure to rpcOptions.onGeneralError', async () => {
71+
const context = await createTestContext()
72+
// Returning `true` tells birpc the error was handled, suppressing its
73+
// default rethrow — matches how a "log and swallow" host would use this.
74+
const onGeneralError = vi.fn(() => true)
75+
const host = '127.0.0.1'
76+
const port = await getPort({ port: 0, host })
77+
const server = await startHttpAndWs({
78+
context,
79+
host,
80+
port,
81+
auth: false,
82+
rpcOptions: { onGeneralError },
83+
})
84+
85+
try {
86+
const raw = new WebSocket(`ws://${host}:${port}`)
87+
await new Promise<void>((resolve, reject) => {
88+
raw.once('open', () => resolve())
89+
raw.once('error', reject)
90+
})
91+
raw.send('not valid json and not structured-clone either')
92+
93+
await vi.waitFor(() => {
94+
expect(onGeneralError).toHaveBeenCalledTimes(1)
95+
})
96+
raw.close()
97+
}
98+
finally {
99+
await server.close()
100+
}
101+
})
102+
})

packages/devframe/src/node/server.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { BirpcGroup } from 'birpc'
1+
import type { BirpcGroup, EventOptions } from 'birpc'
22
import type { Peer } from 'crossws'
33
import type { NodeAdapter } from 'crossws/adapters/node'
44
import type { ConnectionMeta, DevframeNodeContext, DevframeNodeRpcSession, DevframeNodeRpcSessionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunctions } from 'devframe/types'
@@ -80,6 +80,20 @@ export interface StartHttpAndWsOptions {
8080
* observe — but not override — the connect-time trust decision.
8181
*/
8282
onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void
83+
/**
84+
* Forwarded verbatim to the internal `createRpcServer`'s birpc
85+
* `rpcOptions`, alongside the resolver `startHttpAndWs` installs for
86+
* auth/session wiring. Use this so a host that owns its own structured
87+
* diagnostics (e.g. a coded error reporter) keeps seeing RPC failures
88+
* instead of them being silently absorbed by delegating to
89+
* `startHttpAndWs`. Returning `true` from either callback suppresses
90+
* birpc's own error response to the caller — see birpc's
91+
* `EventOptions` for the full contract.
92+
*/
93+
rpcOptions?: Pick<
94+
EventOptions<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>,
95+
'onFunctionError' | 'onGeneralError'
96+
>
8397
/**
8498
* Extra origins to accept on the WS upgrade beyond the loopback default
8599
* (`localhost`/`127.0.0.1`/`::1` and any `Origin`-less request from a
@@ -150,6 +164,10 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise<St
150164
rpcHost.functions,
151165
{
152166
rpcOptions: {
167+
// Forwarded as-is so a host with its own structured diagnostics
168+
// keeps seeing RPC failures; see `StartHttpAndWsOptions.rpcOptions`.
169+
onFunctionError: options.rpcOptions?.onFunctionError,
170+
onGeneralError: options.rpcOptions?.onGeneralError,
153171
// Wrap each RPC handler in an AsyncLocalStorage context so
154172
// `ctx.rpc.getCurrentRpcSession()` works inside handlers (used
155173
// by streaming subscribe/unsubscribe/cancel and shared-state

0 commit comments

Comments
 (0)