fix(gui): harden multimodal image attachments

This commit is contained in:
Yichen Jiang
2026-07-23 19:38:37 +08:00
parent eea595fcb4
commit 580e05b794
61 changed files with 1700 additions and 214 deletions

View File

@@ -1,10 +1,13 @@
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'
import { request as httpRequest } from 'node:http'
import { createServer as createNetServer, Server as NetServer, type AddressInfo } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { startWebServer, type RunningWebServer } from '../src/index.ts'
const MAX_REQUEST_BODY_BYTES = 64 * 1024
/** Reserve a loopback port for tests that need to address a second server. */
function freePort(): Promise<number> {
return new Promise((resolve, reject) => {
@@ -104,17 +107,35 @@ afterEach(async () => {
server = undefined
})
async function boot(onError: (err: Error) => void = () => undefined): Promise<string> {
async function boot(
onError: (err: Error) => void = () => undefined,
maxRequestBodyBytes = MAX_REQUEST_BODY_BYTES,
): Promise<string> {
const { distIndex } = makeDist()
const port = await freePort()
server = await startWebServer({ host: '127.0.0.1', port, distIndex, apiHandler: echoingApi }, onError)
server = await startWebServer({
host: '127.0.0.1', port, distIndex, apiHandler: echoingApi, maxRequestBodyBytes,
}, onError)
return `http://127.0.0.1:${String(server.port)}`
}
describe('startWebServer', () => {
it('rejects an invalid request-body cap before listening', () => {
const { distIndex } = makeDist()
expect(() => startWebServer({
host: '127.0.0.1',
port: 0,
distIndex,
apiHandler: echoingApi,
maxRequestBodyBytes: 0,
}, () => undefined)).toThrow(/positive integer/)
})
it('reports the listening port and closes idempotently', async () => {
const { distIndex } = makeDist()
server = await startWebServer({ host: '127.0.0.1', port: 0, distIndex, apiHandler: echoingApi }, () => undefined)
server = await startWebServer({
host: '127.0.0.1', port: 0, distIndex, apiHandler: echoingApi, maxRequestBodyBytes: MAX_REQUEST_BODY_BYTES,
}, () => undefined)
expect(server.port).toBeGreaterThan(0)
const first = server.close()
const second = server.close()
@@ -136,7 +157,9 @@ describe('startWebServer', () => {
})
const address = vi.spyOn(NetServer.prototype, 'address').mockReturnValue({ address: host, family: 'IPv4', port })
try {
const inertServer = await startWebServer({ host, port, distIndex, apiHandler: echoingApi }, () => undefined)
const inertServer = await startWebServer({
host, port, distIndex, apiHandler: echoingApi, maxRequestBodyBytes: MAX_REQUEST_BODY_BYTES,
}, () => undefined)
expect(listen).toHaveBeenCalledWith(port, host, expect.any(Function))
await inertServer.close()
} finally {
@@ -148,8 +171,12 @@ describe('startWebServer', () => {
it('rejects when the port is already taken', async () => {
const { distIndex } = makeDist()
const port = await freePort()
server = await startWebServer({ host: '127.0.0.1', port, distIndex, apiHandler: echoingApi }, () => undefined)
await expect(startWebServer({ host: '127.0.0.1', port, distIndex, apiHandler: echoingApi }, () => undefined))
server = await startWebServer({
host: '127.0.0.1', port, distIndex, apiHandler: echoingApi, maxRequestBodyBytes: MAX_REQUEST_BODY_BYTES,
}, () => undefined)
await expect(startWebServer({
host: '127.0.0.1', port, distIndex, apiHandler: echoingApi, maxRequestBodyBytes: MAX_REQUEST_BODY_BYTES,
}, () => undefined))
.rejects.toMatchObject({ code: 'EADDRINUSE' })
})
})
@@ -207,7 +234,10 @@ describe.skipIf(process.platform === 'win32')('web plugin surfaces (boot injecti
}
const port = await freePort()
server = await startWebServer(
{ host: '127.0.0.1', port, distIndex, apiHandler: echoingApi, webPlugins }, () => undefined,
{
host: '127.0.0.1', port, distIndex, apiHandler: echoingApi,
maxRequestBodyBytes: MAX_REQUEST_BODY_BYTES, webPlugins,
}, () => undefined,
)
return `http://127.0.0.1:${String(server.port)}`
}
@@ -245,7 +275,10 @@ describe.skipIf(process.platform === 'win32')('web plugin surfaces (boot injecti
}
const port = await freePort()
server = await startWebServer(
{ host: '127.0.0.1', port, distIndex, apiHandler: echoingApi, webPlugins }, () => undefined,
{
host: '127.0.0.1', port, distIndex, apiHandler: echoingApi,
maxRequestBodyBytes: MAX_REQUEST_BODY_BYTES, webPlugins,
}, () => undefined,
)
const res = await fetch(`http://127.0.0.1:${String(server.port)}/plugins/@deepseek-ai/dsh-client-connection/client.js`)
expect(res.status).toBe(404)
@@ -305,6 +338,35 @@ describe('/api bridge', () => {
expect(await response.json()).toEqual({ method: 'POST', body: '{"n":1}', header: 'p1' })
})
it('returns 413 before buffering a declared oversized body', async () => {
const base = await boot()
const response = await fetch(`${base}/api/echo`, {
method: 'POST',
body: 'x'.repeat(MAX_REQUEST_BODY_BYTES + 1),
})
expect(response.status).toBe(413)
})
it('bounds chunked request buffering when no content length is declared', async () => {
const base = await boot(() => undefined, 8)
const target = new URL(`${base}/api/echo`)
const status = await new Promise<number | undefined>((resolve, reject) => {
const request = httpRequest({
hostname: target.hostname,
port: target.port,
path: target.pathname,
method: 'POST',
}, (response) => {
response.resume()
response.on('end', () => { resolve(response.statusCode) })
})
request.on('error', reject)
request.write('12345')
request.end('67890')
})
expect(status).toBe(413)
})
it('relays a bodyless response', async () => {
const base = await boot()
const response = await fetch(`${base}/api/empty`, { method: 'POST' })