From a14158e8185ce89ea0ba5321be36f22104dc8189 Mon Sep 17 00:00:00 2001 From: chuckbutkus Date: Fri, 13 Mar 2026 21:08:23 -0400 Subject: [PATCH] fix: use query params for file upload path (#13376) Co-authored-by: openhands --- .../api/v1-conversation-service.test.ts | 92 ++++++++++++++++++- .../v1-conversation-service.api.ts | 7 +- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/frontend/__tests__/api/v1-conversation-service.test.ts b/frontend/__tests__/api/v1-conversation-service.test.ts index 99039967f1..09b843244a 100644 --- a/frontend/__tests__/api/v1-conversation-service.test.ts +++ b/frontend/__tests__/api/v1-conversation-service.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi, beforeEach, afterEach, Mock } from "vitest"; +import axios from "axios"; import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api"; const { mockGet } = vi.hoisted(() => ({ mockGet: vi.fn() })); @@ -6,6 +7,8 @@ vi.mock("#/api/open-hands-axios", () => ({ openHands: { get: mockGet }, })); +vi.mock("axios"); + describe("V1ConversationService", () => { describe("readConversationFile", () => { it("uses default plan path when filePath is not provided", async () => { @@ -24,4 +27,91 @@ describe("V1ConversationService", () => { ); }); }); + + describe("uploadFile", () => { + beforeEach(() => { + vi.clearAllMocks(); + (axios.post as Mock).mockResolvedValue({ data: {} }); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it("uses query params for file upload path", async () => { + // Arrange + const conversationUrl = "http://localhost:54928/api/conversations/conv-123"; + const sessionApiKey = "test-api-key"; + const file = new File(["test content"], "test.txt", { type: "text/plain" }); + const uploadPath = "/workspace/custom/path.txt"; + + // Act + await V1ConversationService.uploadFile( + conversationUrl, + sessionApiKey, + file, + uploadPath, + ); + + // Assert + expect(axios.post).toHaveBeenCalledTimes(1); + const callUrl = (axios.post as Mock).mock.calls[0][0] as string; + + // Verify URL uses query params format + expect(callUrl).toContain("/api/file/upload?"); + expect(callUrl).toContain("path=%2Fworkspace%2Fcustom%2Fpath.txt"); + + // Verify it's NOT using path params format + expect(callUrl).not.toContain("/api/file/upload/%2F"); + }); + + it("uses default workspace path when no path provided", async () => { + // Arrange + const conversationUrl = "http://localhost:54928/api/conversations/conv-123"; + const sessionApiKey = "test-api-key"; + const file = new File(["test content"], "myfile.txt", { type: "text/plain" }); + + // Act + await V1ConversationService.uploadFile( + conversationUrl, + sessionApiKey, + file, + ); + + // Assert + expect(axios.post).toHaveBeenCalledTimes(1); + const callUrl = (axios.post as Mock).mock.calls[0][0] as string; + + // Default path should be /workspace/{filename} + expect(callUrl).toContain("path=%2Fworkspace%2Fmyfile.txt"); + }); + + it("sends file as FormData with correct headers", async () => { + // Arrange + const conversationUrl = "http://localhost:54928/api/conversations/conv-123"; + const sessionApiKey = "test-api-key"; + const file = new File(["test content"], "test.txt", { type: "text/plain" }); + + // Act + await V1ConversationService.uploadFile( + conversationUrl, + sessionApiKey, + file, + ); + + // Assert + expect(axios.post).toHaveBeenCalledTimes(1); + const callArgs = (axios.post as Mock).mock.calls[0]; + + // Verify FormData is sent + const formData = callArgs[1]; + expect(formData).toBeInstanceOf(FormData); + expect(formData.get("file")).toBe(file); + + // Verify headers include session API key and content type + const headers = callArgs[2].headers; + expect(headers).toHaveProperty("X-Session-API-Key", sessionApiKey); + expect(headers).toHaveProperty("Content-Type", "multipart/form-data"); + }); + }); }); diff --git a/frontend/src/api/conversation-service/v1-conversation-service.api.ts b/frontend/src/api/conversation-service/v1-conversation-service.api.ts index 56942b54c4..17cbb24cdf 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.api.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.api.ts @@ -253,7 +253,7 @@ class V1ConversationService { /** * Upload a single file to the V1 conversation workspace - * V1 API endpoint: POST /api/file/upload/{path} + * V1 API endpoint: POST /api/file/upload?path={path} * * @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...") * @param sessionApiKey Session API key for authentication (required for V1) @@ -269,10 +269,11 @@ class V1ConversationService { ): Promise { // Default to /workspace/{filename} if no path provided (must be absolute) const uploadPath = path || `/workspace/${file.name}`; - const encodedPath = encodeURIComponent(uploadPath); + const params = new URLSearchParams(); + params.append("path", uploadPath); const url = this.buildRuntimeUrl( conversationUrl, - `/api/file/upload/${encodedPath}`, + `/api/file/upload?${params.toString()}`, ); const headers = buildSessionHeaders(sessionApiKey);