forked from EduCraft/curriculum-project-hub
fix(hub): do not crash Hub on missing DocMind input files
Merge missing-file DocMind stream crash fix
This commit is contained in:
@@ -19,7 +19,8 @@ import $DocmindClient, {
|
||||
QueryDocParserStatusRequest,
|
||||
} from "@alicloud/docmind-api20220711";
|
||||
import { RuntimeOptions } from "@alicloud/tea-util";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { createReadStream, type ReadStream } from "node:fs";
|
||||
import { once } from "node:events";
|
||||
import { basename } from "node:path";
|
||||
import type { DocmindCapabilitySecretPayload } from "./types.js";
|
||||
|
||||
@@ -66,6 +67,11 @@ type DocmindConfig = ConstructorParameters<typeof $DocmindClient.default>[0];
|
||||
|
||||
export class AliyunDocmindClient implements CapabilityProviderClient {
|
||||
async parse(credential: DocmindCapabilitySecretPayload, options: DocmindParseOptions): Promise<DocmindParseResult> {
|
||||
// Open first so missing local inputs fail closed before touching the SDK.
|
||||
// Unhandled createReadStream('error') previously crashed the Hub process.
|
||||
const fileName = basename(options.inputFilePath);
|
||||
const fileStream = await openLocalFileStream(options.inputFilePath);
|
||||
|
||||
const config: DocmindConfig = {
|
||||
endpoint: credential.endpoint,
|
||||
accessKeyId: credential.accessKeyId,
|
||||
@@ -75,11 +81,9 @@ export class AliyunDocmindClient implements CapabilityProviderClient {
|
||||
} as DocmindConfig;
|
||||
const client = new $DocmindClient.default(config);
|
||||
|
||||
// 1. Submit job with local file as a ReadStream (not a Buffer — the SDK
|
||||
// serializes Buffers as JSON {type:"Buffer",data:[...]} which the API
|
||||
// can't read; a Stream is uploaded as multipart form data).
|
||||
const fileName = basename(options.inputFilePath);
|
||||
const fileStream = createReadStream(options.inputFilePath);
|
||||
// Submit job with local file as a ReadStream (not a Buffer — the SDK
|
||||
// serializes Buffers as JSON {type:"Buffer",data:[...]} which the API
|
||||
// can't read; a Stream is uploaded as multipart form data).
|
||||
const advanceRequest = new SubmitDocParserJobAdvanceRequest({
|
||||
fileUrlObject: fileStream,
|
||||
fileName,
|
||||
@@ -91,6 +95,7 @@ export class AliyunDocmindClient implements CapabilityProviderClient {
|
||||
try {
|
||||
submitResponse = await client.submitDocParserJobAdvance(advanceRequest, runtime);
|
||||
} catch (e) {
|
||||
fileStream.destroy();
|
||||
throw new DocmindClientError(
|
||||
e instanceof Error ? e.message : String(e),
|
||||
"docmind_unreachable",
|
||||
@@ -229,3 +234,28 @@ function extractFilename(altText: string, url: string, index: number): string {
|
||||
if (base !== "" && base !== "/") return base;
|
||||
return `image_${index + 1}.png`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a local file as a ReadStream only after the fd is successfully open.
|
||||
* createReadStream() emits asynchronous 'error' for missing paths; without a
|
||||
* listener that becomes an unhandled EventEmitter error and exits Node.
|
||||
*/
|
||||
async function openLocalFileStream(path: string): Promise<ReadStream> {
|
||||
const stream = createReadStream(path);
|
||||
try {
|
||||
await once(stream, "open");
|
||||
} catch (error) {
|
||||
stream.destroy();
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === "ENOENT") {
|
||||
throw new DocmindClientError(`input file not found: ${path}`, "docmind_rejected");
|
||||
}
|
||||
throw new DocmindClientError(err.message, "docmind_unreachable");
|
||||
}
|
||||
// After open, residual stream errors must not become unhandled and crash Hub.
|
||||
stream.on("error", () => {
|
||||
// The Aliyun SDK / destroy path owns consumption failures after open.
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AliyunDocmindClient, DocmindClientError } from "../../src/capability/docmindClient.js";
|
||||
|
||||
describe("AliyunDocmindClient local file open", () => {
|
||||
it("rejects a missing input file without crashing the process", async () => {
|
||||
const client = new AliyunDocmindClient();
|
||||
const missing = join(tmpdir(), `docmind-missing-${Date.now()}.pdf`);
|
||||
|
||||
// If createReadStream errors are left unhandled, Vitest aborts the suite
|
||||
// with an unhandled 'error' event instead of reaching this assertion.
|
||||
await expect(client.parse(
|
||||
{
|
||||
accessKeyId: "ak",
|
||||
accessKeySecret: "sk",
|
||||
endpoint: "docmind-api.cn-hangzhou.aliyuncs.com",
|
||||
},
|
||||
{ inputFilePath: missing },
|
||||
)).rejects.toBeInstanceOf(DocmindClientError);
|
||||
|
||||
await expect(client.parse(
|
||||
{
|
||||
accessKeyId: "ak",
|
||||
accessKeySecret: "sk",
|
||||
endpoint: "docmind-api.cn-hangzhou.aliyuncs.com",
|
||||
},
|
||||
{ inputFilePath: missing },
|
||||
)).rejects.toMatchObject({
|
||||
code: "docmind_rejected",
|
||||
message: expect.stringContaining("input file not found"),
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user