forked from bai/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,
|
QueryDocParserStatusRequest,
|
||||||
} from "@alicloud/docmind-api20220711";
|
} from "@alicloud/docmind-api20220711";
|
||||||
import { RuntimeOptions } from "@alicloud/tea-util";
|
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 { basename } from "node:path";
|
||||||
import type { DocmindCapabilitySecretPayload } from "./types.js";
|
import type { DocmindCapabilitySecretPayload } from "./types.js";
|
||||||
|
|
||||||
@@ -66,6 +67,11 @@ type DocmindConfig = ConstructorParameters<typeof $DocmindClient.default>[0];
|
|||||||
|
|
||||||
export class AliyunDocmindClient implements CapabilityProviderClient {
|
export class AliyunDocmindClient implements CapabilityProviderClient {
|
||||||
async parse(credential: DocmindCapabilitySecretPayload, options: DocmindParseOptions): Promise<DocmindParseResult> {
|
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 = {
|
const config: DocmindConfig = {
|
||||||
endpoint: credential.endpoint,
|
endpoint: credential.endpoint,
|
||||||
accessKeyId: credential.accessKeyId,
|
accessKeyId: credential.accessKeyId,
|
||||||
@@ -75,11 +81,9 @@ export class AliyunDocmindClient implements CapabilityProviderClient {
|
|||||||
} as DocmindConfig;
|
} as DocmindConfig;
|
||||||
const client = new $DocmindClient.default(config);
|
const client = new $DocmindClient.default(config);
|
||||||
|
|
||||||
// 1. Submit job with local file as a ReadStream (not a Buffer — the SDK
|
// Submit job with local file as a ReadStream (not a Buffer — the SDK
|
||||||
// serializes Buffers as JSON {type:"Buffer",data:[...]} which the API
|
// serializes Buffers as JSON {type:"Buffer",data:[...]} which the API
|
||||||
// can't read; a Stream is uploaded as multipart form data).
|
// can't read; a Stream is uploaded as multipart form data).
|
||||||
const fileName = basename(options.inputFilePath);
|
|
||||||
const fileStream = createReadStream(options.inputFilePath);
|
|
||||||
const advanceRequest = new SubmitDocParserJobAdvanceRequest({
|
const advanceRequest = new SubmitDocParserJobAdvanceRequest({
|
||||||
fileUrlObject: fileStream,
|
fileUrlObject: fileStream,
|
||||||
fileName,
|
fileName,
|
||||||
@@ -91,6 +95,7 @@ export class AliyunDocmindClient implements CapabilityProviderClient {
|
|||||||
try {
|
try {
|
||||||
submitResponse = await client.submitDocParserJobAdvance(advanceRequest, runtime);
|
submitResponse = await client.submitDocParserJobAdvance(advanceRequest, runtime);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
fileStream.destroy();
|
||||||
throw new DocmindClientError(
|
throw new DocmindClientError(
|
||||||
e instanceof Error ? e.message : String(e),
|
e instanceof Error ? e.message : String(e),
|
||||||
"docmind_unreachable",
|
"docmind_unreachable",
|
||||||
@@ -229,3 +234,28 @@ function extractFilename(altText: string, url: string, index: number): string {
|
|||||||
if (base !== "" && base !== "/") return base;
|
if (base !== "" && base !== "/") return base;
|
||||||
return `image_${index + 1}.png`;
|
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