Skip to content

Commit f93dca6

Browse files
committed
fix(media): decode remote URL fallback filenames
1 parent aef9388 commit f93dca6

3 files changed

Lines changed: 77 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
1515
- Agents/subagents: recover stale completion announces by retrying unsupported transcript-wait wakes without transcript waiting and forcing a message-tool handoff when the requester run is already stale. Fixes #83699. (#83700) Thanks @galiniliev.
1616
- Agents/subagents: skip stale embedded-run wake probes for dormant completion requesters, so late subagent completions go straight to requester-agent/direct handoff instead of producing `reason=no_active_run` queue noise. (#82964) Thanks @galiniliev.
1717
- CLI: retry config snapshot reads after a transient failure so one rejected read no longer poisons later commands in the same process. (#83931) Thanks @honor2030.
18+
- Media: decode URL path basenames before using them as remote media fallback filenames, so files like `My%20Report.pdf` are surfaced as `My Report.pdf`. Fixes #84050.
1819
- WhatsApp: clarify inbound group diagnostics so observed but unregistered groups point to `channels.whatsapp.groups` without changing routing or sender authorization. (#83846) Thanks @neeravmakwana.
1920
- WhatsApp: drain pending outbound deliveries on a 30s periodic timer in addition to the reconnect handler, so messages enqueued while the provider is already connected no longer wait for the next reconnect to send. (#79083) Thanks @Oviemudiaga.
2021
- CLI/TUI: include gateway plugin slash commands in TUI autocomplete, so connected sessions can suggest plugin-owned commands exposed by the running Gateway. (#83640) Thanks @se7en-agent.

src/media/fetch.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,69 @@ describe("readRemoteMediaBuffer", () => {
583583
await expect(fs.readFile(saved.path)).resolves.toStrictEqual(Buffer.from([1, 2, 3, 4]));
584584
});
585585

586+
it("decodes URL path basenames when deriving remote media filenames", async () => {
587+
const fetchImpl = vi.fn(
588+
async () =>
589+
new Response(makeStream([new Uint8Array([1, 2, 3])]), {
590+
status: 200,
591+
headers: { "content-type": "application/pdf" },
592+
}),
593+
);
594+
595+
const saved = await saveRemoteMedia({
596+
url: "https://example.com/files/My%20Report.pdf",
597+
fetchImpl,
598+
lookupFn: makeLookupFn(),
599+
maxBytes: 8,
600+
});
601+
602+
expect(saved.fileName).toBe("My Report.pdf");
603+
});
604+
605+
it("keeps raw URL path basenames when percent escapes are malformed", async () => {
606+
const fetchImpl = vi.fn(
607+
async () =>
608+
new Response(makeStream([new Uint8Array([1, 2, 3])]), {
609+
status: 200,
610+
headers: { "content-type": "application/pdf" },
611+
}),
612+
);
613+
614+
const saved = await saveRemoteMedia({
615+
url: "https://example.com/files/bad%E0%A4%A.pdf",
616+
fetchImpl,
617+
lookupFn: makeLookupFn(),
618+
maxBytes: 8,
619+
});
620+
621+
expect(saved.fileName).toBe("bad%E0%A4%A.pdf");
622+
});
623+
624+
it.each([
625+
["https://example.com/files/reports%2FQ1.pdf", "reports_Q1.pdf"],
626+
["https://example.com/files/reports%5CQ1.pdf", "reports_Q1.pdf"],
627+
])(
628+
"keeps decoded URL fallback separators inside the selected basename",
629+
async (url, fileName) => {
630+
const fetchImpl = vi.fn(
631+
async () =>
632+
new Response(makeStream([new Uint8Array([1, 2, 3])]), {
633+
status: 200,
634+
headers: { "content-type": "application/pdf" },
635+
}),
636+
);
637+
638+
const saved = await saveRemoteMedia({
639+
url,
640+
fetchImpl,
641+
lookupFn: makeLookupFn(),
642+
maxBytes: 8,
643+
});
644+
645+
expect(saved.fileName).toBe(fileName);
646+
},
647+
);
648+
586649
it("saves bodyless successful responses without unbounded buffering", async () => {
587650
const saved = await saveResponseMedia(new Response(null, { status: 204 }), {
588651
sourceUrl: "https://example.com/empty",

src/media/fetch.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ function parseContentDispositionFileName(header?: string | null): string | undef
129129
return undefined;
130130
}
131131

132+
function basenameFromUrlPathname(pathname: string): string {
133+
const base = basenameFromAnyPath(pathname);
134+
if (!base) {
135+
return "";
136+
}
137+
try {
138+
return decodeURIComponent(base).replace(/[\\/]+/g, "_");
139+
} catch {
140+
return base;
141+
}
142+
}
143+
132144
async function readErrorBodySnippet(
133145
res: Response,
134146
opts?: {
@@ -295,7 +307,7 @@ function resolveRemoteFileName(params: {
295307
let fileNameFromUrl: string | undefined;
296308
try {
297309
const parsed = new URL(params.finalUrl);
298-
const base = basenameFromAnyPath(parsed.pathname);
310+
const base = basenameFromUrlPathname(parsed.pathname);
299311
fileNameFromUrl = base || undefined;
300312
} catch {
301313
// ignore parse errors; leave undefined

0 commit comments

Comments
 (0)