Skip to content

Commit 2ad92d7

Browse files
committed
fix(security): release the vendor stream when metering rejects the request
The fail-closed branch returned 500 with the ElevenLabs response body still open, so synthesis and download kept consuming vendor and runtime resources for a caller that was already rejected. Cancel it before returning, and assert the cancellation in the test.
1 parent d1360c5 commit 2ad92d7

2 files changed

Lines changed: 18 additions & 8 deletions

File tree

apps/sim/app/api/proxy/tts/stream/route.test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,26 @@ function validBody(overrides: Record<string, unknown> = {}) {
8282
}
8383
}
8484

85-
/** Minimal ElevenLabs stub returning a readable audio body. */
85+
/**
86+
* Minimal ElevenLabs stub returning a readable audio body. Returns the `cancel`
87+
* spy so tests can assert the vendor stream is released when we reject.
88+
*/
8689
function mockElevenLabsAudio() {
90+
const stream = new ReadableStream({
91+
start(controller) {
92+
controller.enqueue(new Uint8Array([0x49, 0x44, 0x33]))
93+
controller.close()
94+
},
95+
})
96+
const cancel = vi.fn(() => stream.cancel())
8797
global.fetch = vi.fn().mockResolvedValue({
8898
ok: true,
8999
status: 200,
90100
statusText: 'OK',
91-
body: new ReadableStream({
92-
start(controller) {
93-
controller.enqueue(new Uint8Array([0x49, 0x44, 0x33]))
94-
controller.close()
95-
},
96-
}),
101+
body: { getReader: () => stream.getReader(), cancel },
97102
// double-cast-allowed: minimal fetch stub for the ElevenLabs stream call
98103
}) as unknown as typeof fetch
104+
return cancel
99105
}
100106

101107
beforeEach(() => {
@@ -257,14 +263,16 @@ describe('POST /api/proxy/tts/stream — attribution', () => {
257263
expect(mockRecordUsage).not.toHaveBeenCalled()
258264
})
259265

260-
it('refuses to stream audio it could not record a charge for', async () => {
266+
it('refuses to stream audio it could not record a charge for, releasing the vendor stream', async () => {
267+
const cancel = mockElevenLabsAudio()
261268
queueTableRows(schemaMock.chat, [publicChatRow])
262269
mockRecordUsage.mockRejectedValue(new Error('ledger unavailable'))
263270

264271
const res = await POST(createMockRequest('POST', validBody()))
265272

266273
expect(res.status).toBe(500)
267274
expect(res.headers.get('Content-Type')).not.toBe('audio/mpeg')
275+
expect(cancel).toHaveBeenCalledTimes(1)
268276
})
269277

270278
it('rejects an unknown chat without touching the platform key', async () => {

apps/sim/app/api/proxy/tts/stream/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
187187
})
188188
} catch (err) {
189189
logger.error('Failed to record voice output usage, refusing to stream:', err)
190+
// Release the open vendor stream; nothing will drain it once we reject.
191+
await response.body?.cancel().catch(() => {})
190192
return new Response('Unable to record usage for this request', { status: 500 })
191193
}
192194

0 commit comments

Comments
 (0)