Просмотр исходного кода

fix(opencode): forward remote workspace request bodies (#29458)

Kit Langton 3 месяцев назад
Родитель
Сommit
7bafbb309a

+ 2 - 8
packages/opencode/src/server/routes/instance/httpapi/middleware/proxy.ts

@@ -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(

+ 15 - 3
packages/opencode/test/server/httpapi-workspace-routing.test.ts

@@ -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()

+ 10 - 0
packages/opencode/test/server/httpapi-workspace.test.ts

@@ -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" })

+ 16 - 0
packages/opencode/test/server/workspace-proxy.test.ts

@@ -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> = {}