fix(media): harden unknown mime handling from #39199 (thanks @nicolasgrasset)
Co-authored-by: Nicolas Grasset <nicolas.grasset@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user