Skip to content

Commit 4e60ad7

Browse files
clawsweeper[bot]jbetala7Takhoffman
authored
fix(media): decode remote URL fallback filenames (#84108)
Summary: - This replacement PR decodes valid percent escapes in remote media URL fallback basenames, replaces decoded s ... scores, preserves malformed escapes, adds `saveRemoteMedia` regression coverage, and updates the changelog. - Reproducibility: yes. Source inspection plus a Node check on current main show the URL path basename remains `My%20Report.pdf`, and the linked source PR supplies after-fix runtime proof through `saveRemoteMedia`. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(media): decode remote URL fallback filenames Validation: - ClawSweeper review passed for head 8cbac43. - Required merge gates passed before the squash merge. Prepared head SHA: 8cbac43 Review: #84108 (comment) Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent d916f17 commit 4e60ad7

3 files changed

Lines changed: 78 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818
- 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.
1919
- 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.
2020
- 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.
21+
- 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. (#84052) Thanks @jbetala7.
2122
- WhatsApp: clarify inbound group diagnostics so observed but unregistered groups point to `channels.whatsapp.groups` without changing routing or sender authorization. (#83846) Thanks @neeravmakwana.
2223
- 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.
2324
- 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: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,70 @@ 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+
["https://example.com/files/reports%2F%2FQ1.pdf", "reports__Q1.pdf"],
628+
])(
629+
"keeps decoded URL fallback separators inside the selected basename",
630+
async (url, fileName) => {
631+
const fetchImpl = vi.fn(
632+
async () =>
633+
new Response(makeStream([new Uint8Array([1, 2, 3])]), {
634+
status: 200,
635+
headers: { "content-type": "application/pdf" },
636+
}),
637+
);
638+
639+
const saved = await saveRemoteMedia({
640+
url,
641+
fetchImpl,
642+
lookupFn: makeLookupFn(),
643+
maxBytes: 8,
644+
});
645+
646+
expect(saved.fileName).toBe(fileName);
647+
},
648+
);
649+
586650
it("saves bodyless successful responses without unbounded buffering", async () => {
587651
const saved = await saveResponseMedia(new Response(null, { status: 204 }), {
588652
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)