From 0b6b3bbf09efcf8701dc57082af233e3464f838e Mon Sep 17 00:00:00 2001 From: enixCode <58286681+enixCode@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:04:22 +0200 Subject: [PATCH] fix(runner): drop JSON content-type on bodyless light-run requests Service and network teardown silently failed end-to-end. stopRun, the run cancel, and deleteNetwork sent `content-type: application/json` with no body, and Fastify rejects an empty body under that content-type with 400 (FST_ERR_CTP_EMPTY_JSON_BODY). stopServices and the teardown swallowed the error, so the sidecar service container kept running and its run-scoped network leaked (deletion then 409'd on the still-attached container). - add authHeaders() (bearer only, no content-type) for bodyless requests - POST /runs/:id/stop, POST /runs/:id/cancel and DELETE /networks/:name use it build with cc --- src/runner/LightRunClient.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/runner/LightRunClient.ts b/src/runner/LightRunClient.ts index 95870cf..1615476 100644 --- a/src/runner/LightRunClient.ts +++ b/src/runner/LightRunClient.ts @@ -66,7 +66,7 @@ export class LightRunClient { if (!lightRunId) return; fetch(`${this.url}/runs/${lightRunId}/cancel`, { method: 'POST', - headers: this.headers(), + headers: this.authHeaders(), }).catch(() => {}); }; @@ -236,7 +236,7 @@ export class LightRunClient { async deleteNetwork(name: string): Promise { const res = await fetch(`${this.url}/networks/${encodeURIComponent(name)}`, { method: 'DELETE', - headers: this.headers(), + headers: this.authHeaders(), }); if (!res.ok && res.status !== 404) { const text = await res.text().catch(() => ''); @@ -278,7 +278,7 @@ export class LightRunClient { async stopRun(id: string): Promise { const res = await fetch(`${this.url}/runs/${id}/stop`, { method: 'POST', - headers: this.headers(), + headers: this.authHeaders(), }); if (!res.ok && res.status !== 404) { const text = await res.text().catch(() => ''); @@ -291,4 +291,17 @@ export class LightRunClient { if (this.token) h.authorization = `Bearer ${this.token}`; return h; } + + /* + * Headers for a request with NO body (stop, cancel, network delete). A + * bodyless request must not declare `content-type: application/json`: Fastify + * rejects an empty body under that content-type with 400 + * (FST_ERR_CTP_EMPTY_JSON_BODY), which silently failed service/network + * teardown. Carry only the bearer token. + */ + private authHeaders(): Record { + const h: Record = {}; + if (this.token) h.authorization = `Bearer ${this.token}`; + return h; + } }