mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-02 06:16:48 +02:00
fix(opencode): forward remote workspace request bodies (#29458)
This commit is contained in:
@@ -4,17 +4,11 @@ import { HttpBody, HttpClient, HttpClientRequest, HttpServerRequest, HttpServerR
|
|||||||
import * as Socket from "effect/unstable/socket/Socket"
|
import * as Socket from "effect/unstable/socket/Socket"
|
||||||
import { WebSocketTracker } from "../websocket-tracker"
|
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) {
|
function requestBody(request: HttpServerRequest.HttpServerRequest) {
|
||||||
if (request.method === "GET" || request.method === "HEAD") return HttpBody.empty
|
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"]
|
const len = request.headers["content-length"]
|
||||||
return HttpBody.raw(webSource(request)?.body ?? null, {
|
return HttpBody.stream(request.stream, request.headers["content-type"], len ? Number(len) : undefined)
|
||||||
contentType: request.headers["content-type"],
|
|
||||||
contentLength: len ? Number(len) : undefined,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function websocket(
|
export function websocket(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
||||||
import { describe, expect } from "bun:test"
|
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 {
|
import {
|
||||||
FetchHttpClient,
|
FetchHttpClient,
|
||||||
HttpClient,
|
HttpClient,
|
||||||
@@ -64,6 +64,7 @@ type ProxiedRequest = {
|
|||||||
url: string
|
url: string
|
||||||
method: string
|
method: string
|
||||||
headers: Record<string, string>
|
headers: Record<string, string>
|
||||||
|
body: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestHandler<E, R> = (
|
type TestHandler<E, R> = (
|
||||||
@@ -181,7 +182,12 @@ const startRemoteWorkspaceHttpServer = <E, R>(
|
|||||||
// everything else is the request being proxied by the middleware.
|
// everything else is the request being proxied by the middleware.
|
||||||
const sync = syncResponse(request)
|
const sync = syncResponse(request)
|
||||||
if (sync) return yield* sync
|
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.
|
// should make the middleware call HttpApiProxy.http instead.
|
||||||
yield* serveProbe
|
yield* serveProbe
|
||||||
|
|
||||||
|
const body = '{"title":"Remote workspace request"}'
|
||||||
const response = yield* HttpClientRequest.patch(`/probe?workspace=${workspace.id}&keep=yes`).pipe(
|
const response = yield* HttpClientRequest.patch(`/probe?workspace=${workspace.id}&keep=yes`).pipe(
|
||||||
HttpClientRequest.setHeaders({
|
HttpClientRequest.setHeaders({
|
||||||
"content-type": "application/json",
|
|
||||||
"x-opencode-directory": "/secret/path",
|
"x-opencode-directory": "/secret/path",
|
||||||
"x-opencode-workspace": "internal",
|
"x-opencode-workspace": "internal",
|
||||||
}),
|
}),
|
||||||
|
HttpClientRequest.bodyStream(
|
||||||
|
Stream.make(new TextEncoder().encode('{"title":"Remote '), new TextEncoder().encode('workspace request"}')),
|
||||||
|
{ contentType: "application/json" },
|
||||||
|
),
|
||||||
HttpClient.execute,
|
HttpClient.execute,
|
||||||
|
Effect.timeout("2 seconds"),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response.status).toBe(201)
|
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("keep")).toBe("yes")
|
||||||
expect(forwardedURL?.searchParams.get("workspace")).toBeNull()
|
expect(forwardedURL?.searchParams.get("workspace")).toBeNull()
|
||||||
expect(forwarded?.method).toBe("PATCH")
|
expect(forwarded?.method).toBe("PATCH")
|
||||||
|
expect(forwarded?.body).toBe(body)
|
||||||
expect(forwarded?.headers["content-type"]).toBe("application/json")
|
expect(forwarded?.headers["content-type"]).toBe("application/json")
|
||||||
expect(forwarded?.headers["x-target-auth"]).toBe("secret")
|
expect(forwarded?.headers["x-target-auth"]).toBe("secret")
|
||||||
expect(forwarded?.headers["x-opencode-directory"]).toBeUndefined()
|
expect(forwarded?.headers["x-opencode-directory"]).toBeUndefined()
|
||||||
|
|||||||
@@ -479,6 +479,16 @@ describe("workspace HttpApi", () => {
|
|||||||
method: "POST",
|
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 {
|
} finally {
|
||||||
void remote.stop(true)
|
void remote.stop(true)
|
||||||
yield* request(WorkspacePaths.remove.replace(":id", workspace.id), dir, { method: "DELETE" })
|
yield* request(WorkspacePaths.remove.replace(":id", workspace.id), dir, { method: "DELETE" })
|
||||||
|
|||||||
@@ -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", () =>
|
it.live("strips opencode-internal headers and merges extra headers", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
let forwarded: Record<string, string> = {}
|
let forwarded: Record<string, string> = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user