Skip to content

Commit 50fed62

Browse files
committed
fix(ui): replace server action file uploads with route handlers
Server actions cannot reliably serialize File objects in Next.js standalone/docker builds, causing ERR_INCOMPLETE_CHUNKED_ENCODING 500. - Add route handlers for disk, agent skills, and session message uploads - Remove File parameters from server actions - Fix encryption endpoints to use /api/v1 instead of /admin/v1
1 parent 6c7640a commit 50fed62

10 files changed

Lines changed: 185 additions & 145 deletions

File tree

src/server/ui/app/agent_skills/actions.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use server";
22

33
import { ApiResponse } from "@/lib/api-response";
4-
import { API_SERVER_URL, ROOT_API_BEARER_TOKEN, getAuthHeaders, handleResponse, handleError } from "@/lib/api-config";
4+
import { API_SERVER_URL, getAuthHeaders, handleResponse, handleError } from "@/lib/api-config";
55
import { AgentSkill, GetAgentSkillsResp } from "@/types";
66

77
export async function getAgentSkills(
@@ -50,38 +50,6 @@ export async function getAgentSkill(
5050
}
5151
}
5252

53-
export async function createAgentSkill(
54-
file: File,
55-
user?: string,
56-
meta?: string
57-
): Promise<ApiResponse<AgentSkill>> {
58-
try {
59-
const formData = new FormData();
60-
formData.append("file", file);
61-
if (user) {
62-
formData.append("user", user);
63-
}
64-
if (meta) {
65-
formData.append("meta", meta);
66-
}
67-
68-
const response = await fetch(
69-
`${API_SERVER_URL}/api/v1/agent_skills`,
70-
{
71-
method: "POST",
72-
headers: {
73-
Authorization: `Bearer sk-ac-${ROOT_API_BEARER_TOKEN}`,
74-
},
75-
body: formData,
76-
}
77-
);
78-
79-
return await handleResponse<AgentSkill>(response);
80-
} catch (error) {
81-
return handleError(error, "createAgentSkill");
82-
}
83-
}
84-
8553
export async function deleteAgentSkill(
8654
id: string
8755
): Promise<ApiResponse<null>> {

src/server/ui/app/agent_skills/page.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import { Loader2, Upload, RefreshCw } from "lucide-react";
3535
import { PaginationBar } from "@/components/pagination-bar";
3636
import {
3737
getAgentSkills,
38-
createAgentSkill,
3938
deleteAgentSkill,
4039
} from "@/app/agent_skills/actions";
4140
import { AgentSkill } from "@/types";
@@ -139,12 +138,20 @@ export default function AgentSkillsPage() {
139138
try {
140139
setIsUploading(true);
141140
setUploadError("");
142-
const res = await createAgentSkill(
143-
uploadFile,
144-
uploadUser || undefined
145-
);
141+
const formData = new FormData();
142+
formData.append("file", uploadFile);
143+
if (uploadUser) {
144+
formData.append("user", uploadUser);
145+
}
146+
147+
const response = await fetch("/api/agent_skills/upload", {
148+
method: "POST",
149+
body: formData,
150+
});
151+
const res = await response.json();
152+
146153
if (res.code !== 0) {
147-
setUploadError(res.message);
154+
setUploadError(res.msg || res.message);
148155
return;
149156
}
150157
await loadSkills();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
const API_SERVER_URL = process.env.API_SERVER_URL;
4+
const ROOT_API_BEARER_TOKEN = process.env.ROOT_API_BEARER_TOKEN;
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const formData = await request.formData();
9+
10+
const response = await fetch(
11+
`${API_SERVER_URL}/api/v1/agent_skills`,
12+
{
13+
method: "POST",
14+
headers: {
15+
Authorization: `Bearer sk-ac-${ROOT_API_BEARER_TOKEN}`,
16+
},
17+
body: formData,
18+
}
19+
);
20+
21+
const data = await response.json();
22+
return NextResponse.json(data, { status: response.status });
23+
} catch (error) {
24+
console.error("createAgentSkill proxy error:", error);
25+
return NextResponse.json(
26+
{ code: 1, msg: "Internal Server Error" },
27+
{ status: 500 }
28+
);
29+
}
30+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
const API_SERVER_URL = process.env.API_SERVER_URL;
4+
const ROOT_API_BEARER_TOKEN = process.env.ROOT_API_BEARER_TOKEN;
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const formData = await request.formData();
9+
const diskId = formData.get("disk_id") as string;
10+
11+
if (!diskId) {
12+
return NextResponse.json(
13+
{ code: 1, msg: "disk_id is required" },
14+
{ status: 400 }
15+
);
16+
}
17+
18+
// Remove disk_id from the forwarded form data (it's a URL param for the Go API)
19+
formData.delete("disk_id");
20+
21+
const response = await fetch(
22+
`${API_SERVER_URL}/api/v1/disk/${diskId}/artifact`,
23+
{
24+
method: "POST",
25+
headers: {
26+
Authorization: `Bearer sk-ac-${ROOT_API_BEARER_TOKEN}`,
27+
},
28+
body: formData,
29+
}
30+
);
31+
32+
const data = await response.json();
33+
return NextResponse.json(data, { status: response.status });
34+
} catch (error) {
35+
console.error("uploadArtifact proxy error:", error);
36+
return NextResponse.json(
37+
{ code: 1, msg: "Internal Server Error" },
38+
{ status: 500 }
39+
);
40+
}
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
const API_SERVER_URL = process.env.API_SERVER_URL;
4+
const ROOT_API_BEARER_TOKEN = process.env.ROOT_API_BEARER_TOKEN;
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const formData = await request.formData();
9+
const sessionId = formData.get("session_id") as string;
10+
11+
if (!sessionId) {
12+
return NextResponse.json(
13+
{ code: 1, msg: "session_id is required" },
14+
{ status: 400 }
15+
);
16+
}
17+
18+
// Remove session_id from the forwarded form data (it's a URL param for the Go API)
19+
formData.delete("session_id");
20+
21+
const response = await fetch(
22+
`${API_SERVER_URL}/api/v1/session/${sessionId}/messages`,
23+
{
24+
method: "POST",
25+
headers: {
26+
Authorization: `Bearer sk-ac-${ROOT_API_BEARER_TOKEN}`,
27+
},
28+
body: formData,
29+
}
30+
);
31+
32+
const data = await response.json();
33+
return NextResponse.json(data, { status: response.status });
34+
} catch (error) {
35+
console.error("storeMessage proxy error:", error);
36+
return NextResponse.json(
37+
{ code: 1, msg: "Internal Server Error" },
38+
{ status: 500 }
39+
);
40+
}
41+
}

src/server/ui/app/disk/actions.ts

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use server";
22

33
import { ApiResponse } from "@/lib/api-response";
4-
import { API_SERVER_URL, ROOT_API_BEARER_TOKEN, getAuthHeaders, handleResponse, handleError } from "@/lib/api-config";
4+
import { API_SERVER_URL, getAuthHeaders, handleResponse, handleError } from "@/lib/api-config";
55
import { Disk, ListArtifactsResp, GetArtifactResp, GetDisksResp } from "@/types";
66

77
export async function getDisks(
@@ -98,37 +98,6 @@ export async function deleteDisk(disk_id: string): Promise<ApiResponse<null>> {
9898
}
9999
}
100100

101-
export async function uploadArtifact(
102-
disk_id: string,
103-
file_path: string,
104-
file: File,
105-
meta?: Record<string, string>
106-
): Promise<ApiResponse<null>> {
107-
try {
108-
const formData = new FormData();
109-
formData.append("file", file);
110-
formData.append("file_path", file_path);
111-
if (meta && Object.keys(meta).length > 0) {
112-
formData.append("meta", JSON.stringify(meta));
113-
}
114-
115-
const response = await fetch(
116-
`${API_SERVER_URL}/api/v1/disk/${disk_id}/artifact`,
117-
{
118-
method: "POST",
119-
headers: {
120-
Authorization: `Bearer sk-ac-${ROOT_API_BEARER_TOKEN}`,
121-
},
122-
body: formData,
123-
}
124-
);
125-
126-
return await handleResponse<null>(response);
127-
} catch (error) {
128-
return handleError(error, "uploadArtifact");
129-
}
130-
}
131-
132101
export async function deleteArtifact(
133102
disk_id: string,
134103
file_path: string

src/server/ui/app/disk/page.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import {
4444
getArtifact,
4545
createDisk,
4646
deleteDisk,
47-
uploadArtifact,
4847
deleteArtifact,
4948
updateArtifactMeta,
5049
} from "@/app/disk/actions";
@@ -720,15 +719,22 @@ export default function DiskPage() {
720719
setIsUploading(true);
721720
setUploadDialogOpen(false);
722721

723-
const res = await uploadArtifact(
724-
selectedDisk.id,
725-
uploadPath,
726-
selectedUploadFile,
727-
meta
728-
);
722+
const formData = new FormData();
723+
formData.append("disk_id", selectedDisk.id);
724+
formData.append("file", selectedUploadFile);
725+
formData.append("file_path", uploadPath);
726+
if (meta && Object.keys(meta).length > 0) {
727+
formData.append("meta", JSON.stringify(meta));
728+
}
729+
730+
const response = await fetch("/api/disk/upload", {
731+
method: "POST",
732+
body: formData,
733+
});
734+
const res = await response.json();
729735

730736
if (res.code !== 0) {
731-
console.error(res.message);
737+
console.error(res.msg || res.message);
732738
return;
733739
}
734740

src/server/ui/app/encryption/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export async function encryptProject(
2727
): Promise<ApiResponse<null>> {
2828
try {
2929
const response = await fetch(
30-
`${API_SERVER_URL}/admin/v1/project/encrypt`,
30+
`${API_SERVER_URL}/api/v1/project/encrypt`,
3131
{
3232
method: "POST",
3333
headers: {
@@ -47,7 +47,7 @@ export async function decryptProject(
4747
): Promise<ApiResponse<null>> {
4848
try {
4949
const response = await fetch(
50-
`${API_SERVER_URL}/admin/v1/project/decrypt`,
50+
`${API_SERVER_URL}/api/v1/project/decrypt`,
5151
{
5252
method: "POST",
5353
headers: {

src/server/ui/app/session/[sessionId]/messages/page.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -511,17 +511,33 @@ export default function MessagesPage() {
511511

512512
// Build files object
513513
const files = buildFilesObject(uploadedFiles);
514-
515-
// Store message
516-
const res = await storeMessage(
517-
sessionId,
518-
newMessageRole,
519-
parts,
520-
Object.keys(files).length > 0 ? files : undefined
521-
);
514+
const hasFiles = Object.keys(files).length > 0;
515+
516+
let res;
517+
if (hasFiles) {
518+
// Use Route Handler for file uploads (avoids server action File serialization issues)
519+
const formData = new FormData();
520+
formData.append("session_id", sessionId);
521+
const payload = {
522+
blob: { role: newMessageRole, parts },
523+
format: "acontext",
524+
};
525+
formData.append("payload", JSON.stringify(payload));
526+
for (const [fieldName, file] of Object.entries(files)) {
527+
formData.append(fieldName, file);
528+
}
529+
const response = await fetch("/api/session/messages", {
530+
method: "POST",
531+
body: formData,
532+
});
533+
res = await response.json();
534+
} else {
535+
// Use server action for JSON-only messages
536+
res = await storeMessage(sessionId, newMessageRole, parts);
537+
}
522538

523539
if (res.code !== 0) {
524-
console.error(res.message);
540+
console.error(res.msg || res.message);
525541
return;
526542
}
527543

0 commit comments

Comments
 (0)