Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion containers/image-converter/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { promisify } from "node:util";

const execFileAsync = promisify(execFile);
const port = 8080;
const maxInputBytes = 200 * 1024 * 1024;
const maxInputBytes = 100 * 1024 * 1024;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Leave room for multipart overhead in image uploads

When a HEIC/HEIF workspace upload is near the advertised 100 MiB limit, intake accepts it because it compares File.size to workspaceFileUploadLimits.maxFileBytes, but this container compares the HTTP content-length against the same 100 MiB. The conversion client sends the file as multipart/form-data, so the boundary/header bytes make an otherwise valid 100 MiB file exceed this limit and fail during conversion instead of uploading successfully; either allow overhead here or validate the parsed file's size.

Useful? React with 👍 / 👎.

const jpegQuality = 92;
const maxOutputBytes = 1024 * 1024;
const outputProfiles = [
Expand Down
2 changes: 1 addition & 1 deletion containers/liteparse/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const parser = new LiteParse({
quiet: true,
});
const parseTimeoutMs = 90_000;
const maxInputBytes = 200 * 1024 * 1024;
const maxInputBytes = 100 * 1024 * 1024;
const execFileAsync = promisify(execFile);

createServer(async (request, response) => {
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/items.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ ThinkEx accepts these upload families today:
| Images | `.png`, `.jpg`, `.jpeg`, `.webp`, `.heic`, `.heif` |
| Text documents | CSV, TSV, Markdown, code, and plain text imported as documents |

Upload limits are currently 50 files or 200 MB per selection.
Upload limits are currently 50 files or 100 MB per selection.

## Extraction and Previews

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/import-files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ Use file import when you want source material to become part of a workspace.
| Limit | Value |
| --- | --- |
| Files per selection | 50 |
| Bytes per selection | 200 MB |
| Upload concurrency | 5 |
| Bytes per selection | 100 MB |
| Upload concurrency | 3 |

<Tip>
For AI-heavy work, upload only the sources you need for the current workspace. Smaller, well-named workspaces are easier to navigate and easier to ask about.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function WorkspaceFileUploadProvider({
onSuccess: (command) => {
applyWorkspaceEventToCache(queryClient, command.event);
},
}).catch(() => undefined);
});
};

const requestFileSelection = (onSelectFiles: (files: File[]) => void) => {
Expand Down
108 changes: 108 additions & 0 deletions src/features/workspaces/files/workspace-file-upload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const { captureException, toastError, uploadFileDirectlyToR2 } = vi.hoisted(() => ({
captureException: vi.fn(),
toastError: vi.fn(),
uploadFileDirectlyToR2: vi.fn(),
}));

vi.mock("sonner", () => ({
toast: {
error: toastError,
loading: vi.fn(() => "upload-toast"),
success: vi.fn(),
},
}));

vi.mock("#/features/workspaces/upload/workspace-file-direct-upload-client", () => ({
uploadFileDirectlyToR2,
}));

vi.mock("#/features/workspaces/use-workspace-client-mutation-echo", () => ({
prepareWorkspaceClientMutationInput: <T>(input: T) => ({
...input,
clientMutationId: "test-mutation",
}),
}));

vi.mock("#/integrations/posthog/provider", () => ({
capturePostHogClientException: captureException,
}));

import { runWorkspaceFileUploadBatch } from "#/features/workspaces/files/workspace-file-upload";

beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal(
"fetch",
vi.fn().mockImplementation(() =>
Promise.resolve(
new Response(
JSON.stringify({
completionToken: "completion-token",
uploadUrl: "https://r2.example/upload",
}),
{ headers: { "content-type": "application/json" }, status: 200 },
),
),
),
);
});

describe("workspace file upload batch failures", () => {
it("preserves and captures the original upload error once", async () => {
const error = new Error("Direct file upload failed because of a network error.");
uploadFileDirectlyToR2.mockRejectedValue(error);

await runWorkspaceFileUploadBatch({
files: [new File([new Uint8Array([1])], "paper.pdf", { type: "application/pdf" })],
onSuccess: vi.fn(),
parentId: null,
workspaceId: "workspace-id",
});

expect(captureException).toHaveBeenCalledOnce();
expect(captureException).toHaveBeenCalledWith(error, {
operation: "workspace_file_upload",
upload_error_count: 1,
upload_skipped_count: 0,
upload_success_count: 0,
});
});

it("does not capture a canceled upload", async () => {
const error = new DOMException("Upload canceled.", "AbortError");
uploadFileDirectlyToR2.mockRejectedValue(error);

await runWorkspaceFileUploadBatch({
files: [new File([new Uint8Array([1])], "paper.pdf", { type: "application/pdf" })],
onSuccess: vi.fn(),
parentId: null,
workspaceId: "workspace-id",
});

expect(captureException).not.toHaveBeenCalled();
expect(toastError).toHaveBeenCalledWith(
"Upload canceled.",
expect.objectContaining({ id: "upload-toast" }),
);
});

it("does not reclassify a cache callback failure as an upload failure", async () => {
const error = new Error("Cache update failed.");
uploadFileDirectlyToR2.mockResolvedValue(undefined);

await expect(
runWorkspaceFileUploadBatch({
files: [new File([new Uint8Array([1])], "paper.pdf", { type: "application/pdf" })],
onSuccess: () => {
throw error;
},
parentId: null,
workspaceId: "workspace-id",
}),
).rejects.toBe(error);

expect(captureException).not.toHaveBeenCalled();
});
});
Loading
Loading