fix(opencode): forward remote workspace request bodies (#29458)
Some checks are pending
test / e2e (windows) (push) Waiting to run
typecheck / typecheck (push) Waiting to run
deploy / deploy (push) Waiting to run
generate / generate (push) Waiting to run
nix-eval / nix-eval (push) Waiting to run
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404-arm platform_flag:--linux --arm64 target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish (push) Blocked by required conditions
publish / version (push) Waiting to run
publish / build-cli (push) Blocked by required conditions
publish / sign-cli-windows (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
test / unit (linux) (push) Waiting to run
test / unit (windows) (push) Waiting to run
test / e2e (linux) (push) Waiting to run

This commit is contained in:
Kit Langton 2026-05-27 21:54:36 -04:00 committed by GitHub
parent ec26d78450
commit 7bafbb309a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 43 additions and 11 deletions

View file

@ -4,17 +4,11 @@ import { HttpBody, HttpClient, HttpClientRequest, HttpServerRequest, HttpServerR
import * as Socket from "effect/unstable/socket/Socket"
import { WebSocketTracker } from "../websocket-tracker"
function webSource(request: HttpServerRequest.HttpServerRequest): Request | undefined {
return request.source instanceof Request ? request.source : undefined
}
function requestBody(request: HttpServerRequest.HttpServerRequest) {
if (request.method === "GET" || request.method === "HEAD") return HttpBody.empty
if (request.source instanceof Request && request.source.body === null) return HttpBody.empty
const len = request.headers["content-length"]
return HttpBody.raw(webSource(request)?.body ?? null, {
contentType: request.headers["content-type"],
contentLength: len ? Number(len) : undefined,
})
return HttpBody.stream(request.stream, request.headers["content-type"], len ? Number(len) : undefined)
}
export function websocket(

View file

@ -1,6 +1,6 @@
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { describe, expect } from "bun:test"
import { Context, Effect, Layer, Queue, Ref, Schema } from "effect"
import { Context, Effect, Layer, Queue, Ref, Schema, Stream } from "effect"
import {
FetchHttpClient,
HttpClient,
@ -64,6 +64,7 @@ type ProxiedRequest = {
url: string
method: string
headers: Record<string, string>
body: string
}
type TestHandler<E, R> = (
@ -181,7 +182,12 @@ const startRemoteWorkspaceHttpServer = <E, R>(
// everything else is the request being proxied by the middleware.
const sync = syncResponse(request)
if (sync) return yield* sync
return yield* handler({ url: request.url, method: request.method, headers: request.headers })
return yield* handler({
url: request.url,
method: request.method,
headers: request.headers,
body: yield* request.text,
})
}),
)
@ -285,13 +291,18 @@ describe("HttpApi workspace routing middleware", () => {
// should make the middleware call HttpApiProxy.http instead.
yield* serveProbe
const body = '{"title":"Remote workspace request"}'
const response = yield* HttpClientRequest.patch(`/probe?workspace=${workspace.id}&keep=yes`).pipe(
HttpClientRequest.setHeaders({
"content-type": "application/json",
"x-opencode-directory": "/secret/path",
"x-opencode-workspace": "internal",
}),
HttpClientRequest.bodyStream(
Stream.make(new TextEncoder().encode('{"title":"Remote '), new TextEncoder().encode('workspace request"}')),
{ contentType: "application/json" },
),
HttpClient.execute,
Effect.timeout("2 seconds"),
)
expect(response.status).toBe(201)
@ -304,6 +315,7 @@ describe("HttpApi workspace routing middleware", () => {
expect(forwardedURL?.searchParams.get("keep")).toBe("yes")
expect(forwardedURL?.searchParams.get("workspace")).toBeNull()
expect(forwarded?.method).toBe("PATCH")
expect(forwarded?.body).toBe(body)
expect(forwarded?.headers["content-type"]).toBe("application/json")
expect(forwarded?.headers["x-target-auth"]).toBe("secret")
expect(forwarded?.headers["x-opencode-directory"]).toBeUndefined()

View file

@ -479,6 +479,16 @@ describe("workspace HttpApi", () => {
method: "POST",
}),
])
const aborted = yield* request(`http://localhost/session/${session.id}/abort`, dir, { method: "POST" })
expect(aborted.status).toBe(200)
expect(proxied.filter((item) => new URL(item.url).pathname === `/base/session/${session.id}/abort`)).toEqual([
expect.objectContaining({
url: `http://127.0.0.1:${remote.port}/base/session/${session.id}/abort`,
method: "POST",
body: "",
}),
])
} finally {
void remote.stop(true)
yield* request(WorkspacePaths.remove.replace(":id", workspace.id), dir, { method: "DELETE" })

View file

@ -112,6 +112,22 @@ describe("HttpApi workspace proxy", () => {
}),
)
it.live("proxies bodyless Web mutation requests as an empty body", () =>
Effect.gen(function* () {
const url = yield* listenServer(
Effect.fnUntraced(function* (req: HttpServerRequest.HttpServerRequest) {
return yield* HttpServerResponse.json({ method: req.method, body: yield* req.text })
}),
)
const request = HttpServerRequest.fromWeb(new Request("http://localhost/session/abc/abort", { method: "POST" }))
const httpClient = yield* HttpClient.HttpClient
const response = yield* HttpApiProxy.http(httpClient, `${url}/session/abc/abort`, undefined, request)
expect(response.status).toBe(200)
expect(yield* HttpServerResponse.toClientResponse(response).json).toEqual({ method: "POST", body: "" })
}),
)
it.live("strips opencode-internal headers and merges extra headers", () =>
Effect.gen(function* () {
let forwarded: Record<string, string> = {}