fix(chat): lower unavailable media logs

This commit is contained in:
2026-07-14 17:19:10 +08:00
parent 3c48aa7909
commit 49788a04d5
4 changed files with 153 additions and 21 deletions
@@ -1,9 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { Result } from "@/utils";
import { ChatMediaCacheCoordinator } from "../chat_media_cache_coordinator";
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("ChatMediaCacheCoordinator identity isolation", () => {
it("looks up media only in the current identity namespace", async () => {
const getMedia = vi.fn(async () => Result.ok(null));
@@ -27,6 +32,9 @@ describe("ChatMediaCacheCoordinator identity isolation", () => {
});
it("does not fall back to an anonymous cache without an identity", async () => {
const error = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
const getMedia = vi.fn(async () => Result.ok(null));
const coordinator = new ChatMediaCacheCoordinator(
{
@@ -43,5 +51,42 @@ describe("ChatMediaCacheCoordinator identity isolation", () => {
expect(Result.isErr(result)).toBe(true);
expect(getMedia).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(
expect.stringContaining("ERROR [Result] Result.wrap caught exception"),
expect.any(Object),
);
});
it("logs expired remote media responses at info level", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () =>
new Response(null, { status: 400, statusText: "Bad Request" }),
),
);
const info = vi.spyOn(console, "info").mockImplementation(() => undefined);
const error = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
const coordinator = new ChatMediaCacheCoordinator({
getMedia: vi.fn(async () => Result.ok(null)),
saveMedia: vi.fn(async () => Result.ok(undefined)),
});
const result = await coordinator.cacheRemoteMedia(
{
messageId: "message-expired",
kind: "image",
remoteUrl: "https://media.example/expired.jpg",
},
"user:account-a",
);
expect(Result.isErr(result)).toBe(true);
expect(info).toHaveBeenCalledWith(
expect.stringContaining("INFO [Result] Result.wrap caught exception"),
expect.any(Object),
);
expect(error).not.toHaveBeenCalled();
});
});
@@ -27,6 +27,23 @@ import {
const log = new Logger("DataRepositoriesChatMediaCacheCoordinator");
class ChatMediaDownloadError extends Error {
override readonly cause?: unknown;
readonly status?: number;
constructor(message: string, options?: { cause?: unknown; status?: number }) {
super(message);
this.name = "ChatMediaDownloadError";
this.cause = options?.cause;
this.status = options?.status;
}
}
function isChatMediaDownloadError(error: Error): boolean {
if (error instanceof ChatMediaDownloadError) return true;
return error.cause instanceof ChatMediaDownloadError;
}
type ChatMediaStorage = Pick<
LocalChatMediaStorage,
"getMedia" | "saveMedia"
@@ -55,10 +72,16 @@ export class ChatMediaCacheCoordinator {
input: CacheRemoteChatMediaInput,
identity?: string,
): Promise<Result<LocalChatMediaRow>> {
return Result.wrap(async () => {
const ownerKey = await this._requireIdentity(identity);
return this._cacheRemoteMediaOrThrow(input, ownerKey);
});
return Result.wrap(
async () => {
const ownerKey = await this._requireIdentity(identity);
return this._cacheRemoteMediaOrThrow(input, ownerKey);
},
{
errorLogLevel: (error) =>
isChatMediaDownloadError(error) ? "info" : "error",
},
);
}
async prefetchMediaForMessages(
@@ -93,12 +116,19 @@ export class ChatMediaCacheCoordinator {
try {
await this._cacheRemoteMediaOrThrow(target, ownerKey);
} catch (error) {
log.warn("[chat-media] prefetch failed", {
const details = {
messageId: target.messageId,
kind: target.kind,
remoteUrl: target.remoteUrl,
error,
});
};
if (error instanceof ChatMediaDownloadError) {
log.info("[chat-media] remote media unavailable during prefetch", {
...details,
status: error.status,
});
} else {
log.warn("[chat-media] prefetch failed", details);
}
}
}
});
@@ -117,19 +147,34 @@ export class ChatMediaCacheCoordinator {
if (Result.isErr(existingResult)) throw existingResult.error;
if (existingResult.data) return existingResult.data;
const response = await fetch(input.remoteUrl, {
method: "GET",
cache: "force-cache",
});
let response: Response;
try {
response = await fetch(input.remoteUrl, {
method: "GET",
cache: "force-cache",
});
} catch (error) {
throw new ChatMediaDownloadError("Media download request failed.", {
cause: error,
});
}
if (!response.ok) {
throw new Error(
throw new ChatMediaDownloadError(
`Media download failed: ${response.status} ${response.statusText}`,
{ status: response.status },
);
}
const bytes = await response.arrayBuffer();
let bytes: ArrayBuffer;
try {
bytes = await response.arrayBuffer();
} catch (error) {
throw new ChatMediaDownloadError("Media download body could not be read.", {
cause: error,
});
}
if (bytes.byteLength <= 0) {
throw new Error("Media download returned empty bytes.");
throw new ChatMediaDownloadError("Media download returned empty bytes.");
}
const mimeType =
response.headers.get("content-type") ||