Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1487,13 +1487,7 @@ async function forwardOpencodeV2Request(input: {
target.searchParams.set("location[directory]", input.workspace.path);
}

const headers = new Headers(input.request.headers);
headers.delete("authorization");
headers.delete("x-openwork-host-token");
headers.delete("x-openwork-client-id");
headers.delete("host");
headers.delete("origin");
headers.delete("transfer-encoding");
const headers = forwardableProxyRequestHeaders(input.request.headers);
headers.set("authorization", `Basic ${Buffer.from(`opencode:${input.connection.password}`).toString("base64")}`);

// Like the v1 proxy: an engine that is down or restarting is an expected
Expand Down Expand Up @@ -1978,13 +1972,7 @@ async function forwardOpencodeRequest(input: {
throw new ApiError(400, "opencode_unconfigured", "OpenCode base URL is missing for this workspace");
}

let headers = new Headers(input.request.headers);
headers.delete("authorization");
headers.delete("x-openwork-host-token");
headers.delete("x-openwork-client-id");
headers.delete("host");
headers.delete("origin");
headers.delete("transfer-encoding");
let headers = forwardableProxyRequestHeaders(input.request.headers);

const directory = workspace ? resolveOpencodeDirectory(workspace) : null;
let search = input.url.search;
Expand Down Expand Up @@ -2409,6 +2397,35 @@ async function proxyEngineEventStreams(input: {
});
}

/**
* Copy client request headers for forwarding to the engine, dropping
* hop-by-hop and transport-level headers. Node's fetch (undici) rejects some
* of these outright (e.g. `expect: 100-continue` throws NotSupportedError
* "expect header not supported", surfacing as `TypeError: fetch failed`), and
* none of them are meaningful across a proxy hop.
*/
const PROXY_STRIPPED_REQUEST_HEADERS = [
"authorization",
"x-openwork-host-token",
"x-openwork-client-id",
"host",
"origin",
"transfer-encoding",
"expect",
"connection",
"keep-alive",
"proxy-connection",
"upgrade",
"te",
"trailer",
] as const;

export function forwardableProxyRequestHeaders(source: HeadersInit): Headers {
const headers = new Headers(source);
for (const name of PROXY_STRIPPED_REQUEST_HEADERS) headers.delete(name);
return headers;
}

/**
* Strip hop-by-hop and transport-level headers that Bun's native fetch keeps
* in the upstream response even after it has already decoded the body for us.
Expand Down
38 changes: 38 additions & 0 deletions apps/server/test/proxy-request-headers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, test } from "bun:test";
import { forwardableProxyRequestHeaders } from "../src/server.js";

describe("forwardableProxyRequestHeaders", () => {
test("drops expect and hop-by-hop headers that Node fetch (undici) rejects", () => {
const headers = forwardableProxyRequestHeaders({
"Expect": "100-continue",
"Connection": "keep-alive",
"Keep-Alive": "timeout=5",
"Transfer-Encoding": "chunked",
"Upgrade": "h2c",
"TE": "gzip",
"Host": "127.0.0.1:1234",
"Origin": "http://localhost",
"Authorization": "Bearer client",
"X-OpenWork-Host-Token": "secret",
"X-OpenWork-Client-Id": "client",
"Content-Type": "application/json",
"X-Custom": "kept",
});
for (const name of ["expect", "connection", "keep-alive", "transfer-encoding", "upgrade", "te", "host", "origin", "authorization", "x-openwork-host-token", "x-openwork-client-id"]) {
expect(headers.has(name)).toBe(false);
}
expect(headers.get("content-type")).toBe("application/json");
expect(headers.get("x-custom")).toBe("kept");
});

test("sanitized headers are accepted by fetch", async () => {
const server = Bun.serve({ port: 0, fetch: () => new Response("ok") });
try {
const headers = forwardableProxyRequestHeaders({ "Expect": "100-continue", "Content-Type": "application/json" });
const response = await fetch(`http://127.0.0.1:${server.port}/`, { method: "POST", headers, body: "{}" });
expect(await response.text()).toBe("ok");
} finally {
server.stop(true);
}
});
});
Loading