fix(media): harden unknown mime handling from #39199 (thanks @nicolasgrasset)

Co-authored-by: Nicolas Grasset <nicolas.grasset@gmail.com>
This commit is contained in:
Peter Steinberger
2026-03-07 23:30:32 +00:00
parent dc92f2e19d
commit 1aaca517e3
7 changed files with 23 additions and 14 deletions

View File

@@ -302,6 +302,7 @@ Docs: https://docs.openclaw.ai
- Nodes/system.run approval binding: carry prepared approval plans through gateway forwarding and bind interpreter-style script operands across approval to execution, so post-approval script rewrites are denied while unchanged approved script runs keep working. Thanks @tdjackey for reporting.
- Nodes/system.run PowerShell wrapper parsing: treat `pwsh`/`powershell` `-EncodedCommand` forms as shell-wrapper payloads so allowlist mode still requires approval instead of falling back to plain argv analysis. Thanks @tdjackey for reporting.
- Control UI/auth error reporting: map generic browser `Fetch failed` websocket close errors back to actionable gateway auth messages (`gateway token mismatch`, `authentication failed`, `retry later`) so dashboard disconnects stop hiding credential problems. Landed from contributor PR #28608 by @KimGLee. Thanks @KimGLee.
- Media/mime unknown-kind handling: return `undefined` (not `"unknown"`) for missing/unrecognized MIME kinds and use document-size fallback caps for unknown remote media, preventing phantom `<media:unknown>` Signal events from being treated as real messages. (#39199) Thanks @nicolasgrasset.
## 2026.3.2

View File

@@ -3,11 +3,11 @@ export const MAX_AUDIO_BYTES = 16 * 1024 * 1024; // 16MB
export const MAX_VIDEO_BYTES = 16 * 1024 * 1024; // 16MB
export const MAX_DOCUMENT_BYTES = 100 * 1024 * 1024; // 100MB
export type MediaKind = "image" | "audio" | "video" | "document" | "unknown";
export type MediaKind = "image" | "audio" | "video" | "document";
export function mediaKindFromMime(mime?: string | null): MediaKind {
export function mediaKindFromMime(mime?: string | null): MediaKind | undefined {
if (!mime) {
return "unknown";
return undefined;
}
if (mime.startsWith("image/")) {
return "image";
@@ -27,7 +27,7 @@ export function mediaKindFromMime(mime?: string | null): MediaKind {
if (mime.startsWith("application/")) {
return "document";
}
return "unknown";
return undefined;
}
export function maxBytesForKind(kind: MediaKind): number {

View File

@@ -128,7 +128,9 @@ describe("mediaKindFromMime", () => {
{ mime: "text/plain", expected: "document" },
{ mime: "text/csv", expected: "document" },
{ mime: "text/html; charset=utf-8", expected: "document" },
{ mime: "model/gltf+json", expected: "unknown" },
{ mime: "model/gltf+json", expected: undefined },
{ mime: null, expected: undefined },
{ mime: undefined, expected: undefined },
] as const)("classifies $mime", ({ mime, expected }) => {
expect(mediaKindFromMime(mime)).toBe(expected);
});
@@ -136,4 +138,9 @@ describe("mediaKindFromMime", () => {
it("normalizes MIME strings before kind classification", () => {
expect(kindFromMime(" Audio/Ogg; codecs=opus ")).toBe("audio");
});
it("returns undefined for missing or unrecognized MIME kinds", () => {
expect(kindFromMime(undefined)).toBeUndefined();
expect(kindFromMime("model/gltf+json")).toBeUndefined();
});
});

View File

@@ -187,6 +187,6 @@ export function imageMimeFromFormat(format?: string | null): string | undefined
}
}
export function kindFromMime(mime?: string | null): MediaKind {
export function kindFromMime(mime?: string | null): MediaKind | undefined {
return mediaKindFromMime(normalizeMimeType(mime));
}

View File

@@ -576,7 +576,8 @@ export async function sendMessageTelegram(
fileName: media.fileName,
});
const isVideoNote = kind === "video" && opts.asVideoNote === true;
const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind)) ?? "file";
const fileName =
media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind ?? "document")) ?? "file";
const file = new InputFile(media.buffer, fileName);
let caption: string | undefined;
let followUpText: string | undefined;

View File

@@ -457,7 +457,7 @@ describe("local media root guard", () => {
}),
).resolves.toEqual(
expect.objectContaining({
kind: "unknown",
kind: undefined,
}),
);
@@ -468,7 +468,7 @@ describe("local media root guard", () => {
}),
).resolves.toEqual(
expect.objectContaining({
kind: "unknown",
kind: undefined,
}),
);
});
@@ -498,7 +498,7 @@ describe("local media root guard", () => {
}),
).resolves.toEqual(
expect.objectContaining({
kind: "unknown",
kind: undefined,
}),
);
});

View File

@@ -19,7 +19,7 @@ import { resolveUserPath } from "../utils.js";
export type WebMediaResult = {
buffer: Buffer;
contentType?: string;
kind: MediaKind;
kind: MediaKind | undefined;
fileName?: string;
};
@@ -284,12 +284,12 @@ async function loadWebMediaInternal(
const clampAndFinalize = async (params: {
buffer: Buffer;
contentType?: string;
kind: MediaKind;
kind: MediaKind | undefined;
fileName?: string;
}): Promise<WebMediaResult> => {
// If caller explicitly provides maxBytes, trust it (for channels that handle large files).
// Otherwise fall back to per-kind defaults.
const cap = maxBytes !== undefined ? maxBytes : maxBytesForKind(params.kind);
const cap = maxBytes !== undefined ? maxBytes : maxBytesForKind(params.kind ?? "document");
if (params.kind === "image") {
const isGif = params.contentType === "image/gif";
if (isGif || !optimizeImages) {
@@ -324,7 +324,7 @@ async function loadWebMediaInternal(
if (/^https?:\/\//i.test(mediaUrl)) {
// Enforce a download cap during fetch to avoid unbounded memory usage.
// For optimized images, allow fetching larger payloads before compression.
const defaultFetchCap = maxBytesForKind("unknown");
const defaultFetchCap = maxBytesForKind("document");
const fetchCap =
maxBytes === undefined
? defaultFetchCap