

























@@ -215,6 +215,25 @@ function expectFetchDownloadCall(url = "https://example.com/frontend-design.tgz"
215215expect(input.auditContext).toBe("marketplace-plugin-download");
216216}
217217218+function cancelTrackedResponse(init?: ResponseInit): {
219+response: Response;
220+wasCanceled: () => boolean;
221+} {
222+let canceled = false;
223+const stream = new ReadableStream<Uint8Array>({
224+start(controller) {
225+controller.enqueue(new TextEncoder().encode("ignored"));
226+},
227+cancel() {
228+canceled = true;
229+},
230+});
231+return {
232+response: new Response(stream, init),
233+wasCanceled: () => canceled,
234+};
235+}
236+218237function expectRemoteMarketplaceInstallResult(result: unknown) {
219238expectRemoteCloneCommand();
220239expect(String(installPluginInput().path)).toMatch(/[\\/]repo[\\/]plugins[\\/]frontend-design$/);
@@ -696,6 +715,42 @@ describe("marketplace plugins", () => {
696715});
697716});
698717718+it("cancels archive download error bodies before returning structured HTTP errors", async () => {
719+await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
720+const tracked = cancelTrackedResponse({
721+status: 503,
722+statusText: "Service Unavailable",
723+});
724+const release = vi.fn(async () => undefined);
725+fetchWithSsrFGuardMock.mockResolvedValueOnce({
726+response: tracked.response,
727+finalUrl: "https://example.com/frontend-design.tgz",
728+ release,
729+});
730+const manifestPath = await writeMarketplaceManifest(rootDir, {
731+plugins: [
732+{
733+name: "frontend-design",
734+source: "https://example.com/frontend-design.tgz",
735+},
736+],
737+});
738+739+const result = await installPluginFromMarketplace({
740+marketplace: manifestPath,
741+plugin: "frontend-design",
742+});
743+744+expect(result).toEqual({
745+ok: false,
746+error: "failed to download https://example.com/frontend-design.tgz: HTTP 503",
747+});
748+expect(tracked.wasCanceled()).toBe(true);
749+expect(installPluginFromPathMock).not.toHaveBeenCalled();
750+expect(release).toHaveBeenCalledTimes(1);
751+});
752+});
753+699754it("returns a structured error for invalid archive URLs", async () => {
700755await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
701756const manifestPath = await writeMarketplaceManifest(rootDir, {
@@ -847,11 +902,12 @@ describe("marketplace plugins", () => {
847902it("rejects non-streaming archive responses before buffering them", async () => {
848903await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
849904const arrayBuffer = vi.fn(async () => new Uint8Array([1, 2, 3]).buffer);
905+const cancel = vi.fn(async () => undefined);
850906fetchWithSsrFGuardMock.mockResolvedValueOnce({
851907response: {
852908ok: true,
853909status: 200,
854-body: {} as Response["body"],
910+body: { cancel } as unknown as Response["body"],
855911headers: new Headers(),
856912 arrayBuffer,
857913} as unknown as Response,
@@ -879,6 +935,7 @@ describe("marketplace plugins", () => {
879935"streaming response body unavailable",
880936});
881937expect(arrayBuffer).not.toHaveBeenCalled();
938+expect(cancel).toHaveBeenCalledTimes(1);
882939expect(installPluginFromPathMock).not.toHaveBeenCalled();
883940});
884941});
@@ -933,12 +990,15 @@ describe("marketplace plugins", () => {
933990"download too large: 268435457 bytes (limit: 268435456 bytes)",
934991});
935992expect(arrayBuffer).not.toHaveBeenCalled();
993+expect(reader.cancel).toHaveBeenCalledTimes(1);
994+expect(reader.releaseLock).toHaveBeenCalledTimes(1);
936995expect(installPluginFromPathMock).not.toHaveBeenCalled();
937996});
938997});
939998940999it("rejects malformed archive content-length headers before streaming", async () => {
9411000await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
1001+const cancel = vi.fn(async () => undefined);
9421002const reader = {
9431003read: vi.fn(),
9441004cancel: vi.fn(async () => undefined),
@@ -950,6 +1010,7 @@ describe("marketplace plugins", () => {
9501010status: 200,
9511011body: {
9521012getReader: () => reader,
1013+ cancel,
9531014} as unknown as Response["body"],
9541015headers: new Headers({ "content-length": "1e9" }),
9551016} as unknown as Response,
@@ -977,6 +1038,56 @@ describe("marketplace plugins", () => {
9771038"invalid content-length header: 1e9",
9781039});
9791040expect(reader.read).not.toHaveBeenCalled();
1041+expect(reader.cancel).not.toHaveBeenCalled();
1042+expect(cancel).toHaveBeenCalledTimes(1);
1043+expect(installPluginFromPathMock).not.toHaveBeenCalled();
1044+});
1045+});
1046+1047+it("rejects oversized archive content-length headers before streaming", async () => {
1048+await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
1049+const cancel = vi.fn(async () => undefined);
1050+const reader = {
1051+read: vi.fn(),
1052+cancel: vi.fn(async () => undefined),
1053+releaseLock: vi.fn(),
1054+};
1055+fetchWithSsrFGuardMock.mockResolvedValueOnce({
1056+response: {
1057+ok: true,
1058+status: 200,
1059+body: {
1060+getReader: () => reader,
1061+ cancel,
1062+} as unknown as Response["body"],
1063+headers: new Headers({ "content-length": String(256 * 1024 * 1024 + 1) }),
1064+} as unknown as Response,
1065+finalUrl: "https://cdn.example.com/releases/frontend-design.tgz",
1066+release: vi.fn(async () => undefined),
1067+});
1068+const manifestPath = await writeMarketplaceManifest(rootDir, {
1069+plugins: [
1070+{
1071+name: "frontend-design",
1072+source: "https://example.com/frontend-design.tgz",
1073+},
1074+],
1075+});
1076+1077+const result = await installPluginFromMarketplace({
1078+marketplace: manifestPath,
1079+plugin: "frontend-design",
1080+});
1081+1082+expect(result).toEqual({
1083+ok: false,
1084+error:
1085+"failed to download https://example.com/frontend-design.tgz: " +
1086+"download too large: 268435457 bytes (limit: 268435456 bytes)",
1087+});
1088+expect(reader.read).not.toHaveBeenCalled();
1089+expect(reader.cancel).not.toHaveBeenCalled();
1090+expect(cancel).toHaveBeenCalledTimes(1);
9801091expect(installPluginFromPathMock).not.toHaveBeenCalled();
9811092});
9821093});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。