From d794435cc8887d60f1d01b546e6afb91e057ad18 Mon Sep 17 00:00:00 2001 From: jacksonriding <99007683+jacksonriding@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:44:33 +1000 Subject: [PATCH] fix: accept bounded large egress grant headers --- service/Dockerfile | 2 +- service/Dockerfile.egress-gateway | 4 +-- service/package.json | 2 +- service/src/egress-headers.test.ts | 49 ++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 service/src/egress-headers.test.ts diff --git a/service/Dockerfile b/service/Dockerfile index d7bf9036..0a872c62 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -59,7 +59,7 @@ ENV NODE_ENV=production WORKDIR /app COPY --from=install /temp/prod/node_modules ./node_modules COPY --from=builder /app/.build-egress-gateway ./.build-egress-gateway -CMD ["bun", "run", ".build-egress-gateway/egress-gateway.js"] +CMD ["bun", "run", "--max-http-header-size=262144", ".build-egress-gateway/egress-gateway.js"] # Development stage FROM base AS development diff --git a/service/Dockerfile.egress-gateway b/service/Dockerfile.egress-gateway index bd1d2b64..a3b8aee7 100644 --- a/service/Dockerfile.egress-gateway +++ b/service/Dockerfile.egress-gateway @@ -22,7 +22,7 @@ COPY packages/code/src /packages/code/src EXPOSE 3190 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:3190/health || exit 1 -CMD ["bun", "run", "src/egress-gateway.ts"] +CMD ["bun", "run", "--max-http-header-size=262144", "src/egress-gateway.ts"] FROM base AS development ENV NODE_ENV=development @@ -31,4 +31,4 @@ COPY service/src ./src COPY shared /shared COPY packages/code/src /packages/code/src EXPOSE 3190 9230 -CMD ["bun", "run", "--watch", "src/egress-gateway.ts"] +CMD ["bun", "run", "--watch", "--max-http-header-size=262144", "src/egress-gateway.ts"] diff --git a/service/package.json b/service/package.json index a3679c8f..8a19c3f8 100644 --- a/service/package.json +++ b/service/package.json @@ -14,7 +14,7 @@ "start:api:dev": "ts-node src/api-server.ts", "start:worker": "node .build-service/src/worker-server.js", "start:worker:dev": "ts-node src/worker-server.ts", - "start:egress-gateway": "bun run src/egress-gateway.ts", + "start:egress-gateway": "bun run --max-http-header-size=262144 src/egress-gateway.ts", "build": "rollup -c", "deploy": "serverless deploy", "test": "bun test ./src/*.test.ts ./src/*.spec.ts ./src/**/*.test.ts ./src/**/*.spec.ts", diff --git a/service/src/egress-headers.test.ts b/service/src/egress-headers.test.ts new file mode 100644 index 00000000..9591acf2 --- /dev/null +++ b/service/src/egress-headers.test.ts @@ -0,0 +1,49 @@ +import { test, expect } from 'bun:test'; +import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +test('deployed egress command accepts scoped grants above 16 KiB, with a finite cap', async () => { + const dockerfile = readFileSync(new URL('../Dockerfile.egress-gateway', import.meta.url), 'utf8'); + const command: string[] = JSON.parse(dockerfile.match(/^CMD (.+)$/m)![1]); + const bundledDockerfile = readFileSync(new URL('../Dockerfile', import.meta.url), 'utf8'); + const commands: string[][] = [...dockerfile.matchAll(/^CMD (.+)$/gm), + ...bundledDockerfile.matchAll(/^CMD (.+egress-gateway.+)$/gm)] + .map(match => JSON.parse(match[1])); + const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); + commands.push(packageJson.scripts['start:egress-gateway'].split(' ')); + expect(commands).toHaveLength(4); + for (const entrypoint of commands) expect(entrypoint).toContain('--max-http-header-size=262144'); + const probe = ` + const http = require('node:http'); + const server = http.createServer((req, res) => res.end('ok')); + server.listen(0, '127.0.0.1', async () => { + try { + const statuses = []; + for (const size of [32768, 280000]) { + const response = await fetch('http://127.0.0.1:' + server.address().port, { + headers: { 'X-CodeAPI-Egress-Grant': 'a'.repeat(size) }, + }); + statuses.push(response.status); + await response.text(); + } + console.log(JSON.stringify(statuses)); + } finally { server.close(); } + });`; + const directory = mkdtempSync(join(tmpdir(), 'codeapi-header-test-')); + const script = join(directory, 'probe.cjs'); + writeFileSync(script, probe); + const run = async (options: string[]) => { + const child = Bun.spawn([process.execPath, ...options, script], { stdout: 'pipe', stderr: 'pipe' }); + const stdout = await new Response(child.stdout).text(); + const stderr = await new Response(child.stderr).text(); + if (await child.exited !== 0) throw new Error(stderr); + expect(await child.exited).toBe(0); + return JSON.parse(stdout); + }; + try { + expect(await run(['run'])).toEqual([431, 431]); + // Preserve the deployed command's argument order, replacing only the file. + expect(await run(command.slice(1, -1))).toEqual([200, 431]); + } finally { rmSync(directory, { recursive: true }); } +});