diff --git a/plugins/mock/src/lib/app.ts b/plugins/mock/src/lib/app.ts index 8c3ff02f9a..6710ab4aeb 100644 --- a/plugins/mock/src/lib/app.ts +++ b/plugins/mock/src/lib/app.ts @@ -99,13 +99,15 @@ class MockApplicationWorker extends Base { this[APP_INIT] = true; debug('this[APP_INIT] = true'); this.#bindEvent(); + // Koa normally installs this in callback(), but startup can emit errors before then. + if (!app.listenerCount('error')) { + app.on('error', (app as any).onerror.bind(app)); + } + // Wait for middleware loading before app.callback() composes the request handler. + await app.ready(); debug('http server instantiate'); createServer(app); - await app.ready(); - // emit `server` after ready: egg core registers its `once('server', ...)` - // listener inside `Application.load()` (during `app.ready()` above), and - // `onServer` reads loaded config, so the event must be emitted now rather - // than at createServer() time. Mirrors @eggjs/cluster's post-ready emit. + // Application.load() registers the onServer listener during app.ready(). if (app.server) { app.emit('server', app.server); } diff --git a/plugins/mock/src/lib/mock_http_server.ts b/plugins/mock/src/lib/mock_http_server.ts index efb0d1fd14..126d15ee03 100644 --- a/plugins/mock/src/lib/mock_http_server.ts +++ b/plugins/mock/src/lib/mock_http_server.ts @@ -12,11 +12,7 @@ export function createServer(app: any): Server { app.server = server; } } - // NOTE: don't emit the `server` event here. egg core registers its - // `once('server', ...)` listener inside `Application.load()` (during - // `app.ready()`), so emitting at server-creation time (before ready) would be - // missed and `onServer` (clientError logging / graceful / timeout / websocket) - // never runs. The caller emits `server` after `app.ready()` instead — just - // like @eggjs/cluster, which emits it after the app is ready. + // Mock startup emits `server` once after app.ready(). HTTP requests also use + // this helper to get the cached server and must not emit the event again. return server; } diff --git a/plugins/mock/src/lib/parallel/app.ts b/plugins/mock/src/lib/parallel/app.ts index 969369e77f..e09d1794fa 100644 --- a/plugins/mock/src/lib/parallel/app.ts +++ b/plugins/mock/src/lib/parallel/app.ts @@ -53,11 +53,15 @@ export class MockParallelApplication extends Base { this.__APP_INIT__ = true; debug('this[APP_INIT] = true'); this.#bindEvents(); + // Koa normally installs this in callback(), but startup can emit errors before then. + if (!app.listenerCount('error')) { + app.on('error', (app as any).onerror.bind(app)); + } + // Wait for middleware loading before app.callback() composes the request handler. + await app.ready(); debug('http server instantiate'); createServer(app); - await app.ready(); - // emit `server` after ready so egg core's onServer listener (registered in - // Application.load()) is wired up; createServer no longer emits it. + // Application.load() registers the onServer listener during app.ready(). if (app.server) { app.emit('server', app.server); } diff --git a/plugins/mock/test/app.test.ts b/plugins/mock/test/app.test.ts index ca1c2462b5..3d948f69f1 100644 --- a/plugins/mock/test/app.test.ts +++ b/plugins/mock/test/app.test.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'node:assert'; +import { request } from '@eggjs/supertest'; import { describe, it, beforeAll, afterAll, afterEach } from 'vitest'; import mm, { type MockApplication } from '../src/index.ts'; @@ -81,12 +82,8 @@ describe('test/app.test.ts', { concurrent: false }, () => { }); it('should run onServer after ready (server event not lost)', async () => { - // @eggjs/mock emits the `server` event before `app.ready()`, while egg core - // registers its `once("server", ...)` listener inside Application.load() - // (during app.ready()). egg core re-emits `server` after the listener is - // registered so onServer still runs and wires up the `clientError` handler - // (plus graceful shutdown / server timeout / websocket). Regression guard: - // assert the clientError listener is wired after ready. + // Application.load() registers the server listener during app.ready(). + // Mock startup must emit server afterward so onServer attaches clientError. const baseDir = getFixtures('server'); const app = mm.app({ baseDir, @@ -104,7 +101,23 @@ describe('test/app.test.ts', { concurrent: false }, () => { } }); - it('should emit server after ready in parallel app', async () => { + it('should compose HTTP middleware after app is ready', async () => { + const app = mm.app({ + baseDir: getFixtures('app'), + framework: getFixtures('snapshot-framework'), + cache: false, + }); + try { + await app.ready(); + assert(app.server, 'app.server not exists'); + await request(app.server).get('/').expect(200, 'foo'); + await app.httpRequest().get('/').expect(200, 'foo'); + } finally { + await app.close(); + } + }); + + it('should create and emit server after ready in parallel app', async () => { // cover the parallel app worker's post-ready `server` emit (lib/parallel/app.ts) const baseDir = getFixtures('server'); let emittedServer: unknown; @@ -127,6 +140,7 @@ describe('test/app.test.ts', { concurrent: false }, () => { assert(app.server, 'app.server not exists'); assert.equal(emittedServer, app.server); assert.equal(emittedAfterReady, true); + await request(app.server).get('/').expect(200, 'ok'); } finally { await app.close(); } @@ -159,6 +173,31 @@ describe('test/app.test.ts', { concurrent: false }, () => { await app.close(); }); + it.each([false, true])('should handle parallel startup errors (custom listener: %s)', async (customListener) => { + const error = new Error('parallel startup failed'); + const errors: Error[] = []; + const app = createParallelApp({ + baseDir: getFixtures('server'), + framework: getFixtures('parallel-framework'), + cache: false, + clean: false, + beforeInit: async (parallelApp) => { + parallelApp.options.clusterPort = 1; + parallelApp.options.readyError = error; + }, + }); + if (customListener) { + app.on('error', (err: Error) => errors.push(err)); + } + try { + await assert.rejects(app.ready(), (err) => err === error); + assert.deepEqual((app as any).errors, customListener ? [] : [error]); + assert.deepEqual(errors, customListener ? [error] : []); + } finally { + await app.close(); + } + }); + // Node.js v20: SyntaxError: Unexpected identifier 'SingleModeApplication' it.skipIf(process.version.startsWith('v20.'))('should FrameworkErrorformater work during app boot', async () => { // let logMsg = ''; diff --git a/plugins/mock/test/fixtures/parallel-framework/index.js b/plugins/mock/test/fixtures/parallel-framework/index.js index 098c5dd5f8..45f8b491df 100644 --- a/plugins/mock/test/fixtures/parallel-framework/index.js +++ b/plugins/mock/test/fixtures/parallel-framework/index.js @@ -6,6 +6,7 @@ class Application extends EventEmitter { this.options = options; this.context = {}; this.config = {}; + this.errors = []; this.messenger = { messages: [], onMessage: (msg) => this.messenger.messages.push(msg), @@ -13,13 +14,25 @@ class Application extends EventEmitter { } callback() { - return (_req, res) => res.end('ok'); + const ready = this.readyAt; + return (_req, res) => { + res.statusCode = ready ? 200 : 503; + res.end(ready ? 'ok' : 'not ready'); + }; } async ready() { + if (this.options.readyError) { + this.emit('error', this.options.readyError); + throw this.options.readyError; + } this.readyAt = true; } + onerror(err) { + this.errors.push(err); + } + async close() {} } diff --git a/plugins/mock/test/fixtures/snapshot-framework/index.js b/plugins/mock/test/fixtures/snapshot-framework/index.js new file mode 100644 index 0000000000..c49b0f2ced --- /dev/null +++ b/plugins/mock/test/fixtures/snapshot-framework/index.js @@ -0,0 +1,17 @@ +const egg = require('egg'); + +class Application extends egg.Application { + callback() { + // Model middleware composition that captures the current list, as koa-compose 4.2 does. + const middleware = this.middleware; + this.middleware = [...middleware]; + try { + return super.callback(); + } finally { + this.middleware = middleware; + } + } +} + +exports.Application = Application; +exports.Agent = egg.Agent;