Files
nexu-io--open-design/apps/daemon/tests/vela-proxy-pipe-error.test.ts
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

59 lines
2.6 KiB
TypeScript

// Regression: the AMR/vela API proxy pipes the upstream response body to the
// client (`upstreamRes.pipe(res)`) and the client upload body to the upstream
// (`req.pipe(upstream)`). `.pipe()` does not forward a source `'error'`, and a
// stream that emits `'error'` with no listener throws — an unhandled exception
// that crashes the privileged daemon. A mid-stream upstream ECONNRESET, or a
// client aborting an upload, would therefore take the whole daemon down. Only
// the outbound ClientRequest had a handler; the two piped stream bodies did not.
//
// `pipeProxyStreamWithGuard` attaches the missing source-error handler. This
// spec pins the crash vector (a bare pipe throws on a source error) against the
// fix (the guard routes the error to the handler and nothing throws).
import { PassThrough } from 'node:stream';
import { describe, expect, it, vi } from 'vitest';
import { pipeProxyStreamWithGuard } from '../src/routes/vela.js';
const flush = () => new Promise((resolve) => setTimeout(resolve, 20));
describe('vela proxy pipe error guard', () => {
it('CONTROL: a bare pipe (the pre-fix shape) throws on a source error — the daemon-crash vector', () => {
const source = new PassThrough();
const dest = new PassThrough();
source.pipe(dest); // exactly what upstreamRes.pipe(res) / req.pipe(upstream) did
expect(source.listenerCount('error')).toBe(0);
// An 'error' with no listener throws (Node would surface this as an
// uncaughtException and kill the daemon).
expect(() => source.emit('error', new Error('ECONNRESET'))).toThrow('ECONNRESET');
});
it('routes a mid-stream source error to the handler instead of crashing', async () => {
const source = new PassThrough();
const dest = new PassThrough();
const onError = vi.fn();
pipeProxyStreamWithGuard(source, dest, onError);
expect(source.listenerCount('error')).toBe(1);
// The same error that threw above must now be delivered to the handler and
// must NOT throw (test completing is itself the no-crash proof).
expect(() => source.emit('error', new Error('ECONNRESET'))).not.toThrow();
await flush();
expect(onError).toHaveBeenCalledOnce();
expect(onError.mock.calls[0]?.[0]).toBeInstanceOf(Error);
});
it('still pipes the payload through on the happy path', async () => {
const source = new PassThrough();
const dest = new PassThrough();
const chunks: Buffer[] = [];
dest.on('data', (c: Buffer) => chunks.push(c));
pipeProxyStreamWithGuard(source, dest, () => {});
source.end(Buffer.from('hello'));
await flush();
expect(Buffer.concat(chunks).toString()).toBe('hello');
});
});