From 629626c341142616fa3bbf458c359b4b507968d2 Mon Sep 17 00:00:00 2001 From: "zhiyi.wang" Date: Mon, 8 Jun 2026 16:14:01 +0800 Subject: [PATCH] fix(opencode): graceful error handling for PDF/image file read failures Previously, when a PDF or image file was unreadable (corrupted, permissions error, truncated, etc.), the session would crash because: 1. prompt.ts used Effect.catch(Effect.die) to convert read errors into unrecoverable defects during user attachment processing 2. read.ts let fs.readFile errors propagate as raw defects through Effect.orDie 3. transform.ts had no validation for empty/corrupted PDF base64 data This commit fixes all three crash paths: - prompt.ts: Replace Effect.catch(Effect.die) with Effect.exit + Exit.isFailure pattern (consistent with text/plain and directory handling in same file), converting read failures into user-visible error messages instead of defects - read.ts: Wrap fs.readFile in Effect.catch to produce a descriptive error message that surfaces through the tool-error event to the model - transform.ts: Add empty base64 PDF data check (mirrors existing empty image check) to catch corrupted PDFs before they hit the provider API Fixes #21390 --- packages/opencode/src/provider/transform.ts | 15 ++++++++++++++ packages/opencode/src/session/prompt.ts | 23 ++++++++++++++++++--- packages/opencode/src/tool/read.ts | 13 ++++++++++-- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 79cfa3ea508a..da11f565d517 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -392,6 +392,21 @@ function unsupportedParts(msgs: ModelMessage[], model: Provider.Model): ModelMes } } + // Check for empty base64 PDF data + if (part.type === "file" && part.mediaType === "application/pdf") { + const url = "url" in part ? String(part.url) : "" + if (url.startsWith("data:")) { + const match = url.match(/^data:([^;]+);base64,(.*)$/) + if (match && (!match[2] || match[2].length === 0)) { + const name = part.filename ? `"${part.filename}"` : "PDF" + return { + type: "text" as const, + text: `ERROR: ${name} file is empty or corrupted. Please provide a valid PDF file.`, + } + } + } + } + const mime = part.type === "image" ? String(part.image).split(";")[0].replace("data:", "") : part.mediaType const filename = part.type === "file" ? part.filename : undefined const modality = mimeToModality(mime) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 701c632345fe..091d0b701947 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -975,6 +975,25 @@ export const layer = Layer.effect( ] } + const readExit = yield* fsys.readFile(filepath).pipe(Effect.exit) + if (Exit.isFailure(readExit)) { + const error = Cause.squash(readExit.cause) + log.error("failed to read file attachment", { error, filepath }) + const message = error instanceof Error ? error.message : String(error) + yield* events.publish(Session.Event.Error, { + sessionID: input.sessionID, + error: new NamedError.Unknown({ message }).toObject(), + }) + return [ + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Failed to read file ${filepath}: ${message}`, + }, + ] + } return [ { messageID: info.id, @@ -988,9 +1007,7 @@ export const layer = Layer.effect( messageID: info.id, sessionID: input.sessionID, type: "file", - url: - `data:${mime};base64,` + - Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"), + url: `data:${mime};base64,${Buffer.from(readExit.value).toString("base64")}`, mime, filename: part.filename!, source: part.source, diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 7aa12659792a..d0bc0c832215 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -310,8 +310,17 @@ export const ReadTool = Tool.define< const isImage = SUPPORTED_IMAGE_MIMES.has(mime) if (isImage || isPdfAttachment(mime)) { - const bytes = yield* fs.readFile(filepath) - const msg = isPdfAttachment(mime) ? "PDF read successfully" : "Image read successfully" + const kind = isPdfAttachment(mime) ? "PDF" : "image" + const bytes = yield* fs.readFile(filepath).pipe( + Effect.catch((error) => + Effect.fail( + new Error( + `Cannot read ${kind} file: ${filepath} (${error instanceof Error ? error.message : String(error)})`, + ), + ), + ), + ) + const msg = `${kind === "PDF" ? "PDF" : "Image"} read successfully` return { title, output: msg,