Skip to content

Commit 241cb8a

Browse files
authored
Merge pull request #29 from script-development/security/download-request-revoke-url
security(http): revoke object URL after downloadRequest click
2 parents 0e4de87 + 058eeb0 commit 241cb8a

4 files changed

Lines changed: 32 additions & 5 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/http/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@script-development/fs-http",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "Framework-agnostic HTTP service factory with middleware architecture",
55
"homepage": "https://packages.script.nl/packages/http",
66
"license": "MIT",

packages/http/src/http.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ export const createHttpService = (baseURL: string, options?: HttpServiceOptions)
112112
link.href = window.URL.createObjectURL(blob);
113113
link.download = documentName;
114114
link.click();
115+
// Revoke the object URL after the click so the browser can release the
116+
// blob reference. The download itself captures its own reference during
117+
// link.click(), so revoking here does not interrupt it.
118+
window.URL.revokeObjectURL(link.href);
115119

116120
return response;
117121
};

packages/http/tests/http.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ beforeEach(() => {
2424
vi.stubGlobal("window", {
2525
URL: {
2626
createObjectURL: vi.fn(() => "blob:http://localhost/fake-object-url"),
27+
revokeObjectURL: vi.fn(),
2728
},
2829
});
2930

@@ -514,6 +515,28 @@ describe("createHttpService", () => {
514515
"No content type found",
515516
);
516517
});
518+
519+
it("revokes the object URL after triggering the download", async () => {
520+
// Arrange
521+
mock.onGet(`${BASE_URL}/download/file.pdf`).reply(200, "file-content", {
522+
"content-type": "application/pdf",
523+
});
524+
const service = createHttpService(BASE_URL);
525+
const mockLink = { href: "", download: "", click: vi.fn() };
526+
vi.stubGlobal("document", {
527+
createElement: vi.fn(() => mockLink),
528+
cookie: "",
529+
});
530+
531+
// Act
532+
await service.downloadRequest("/download/file.pdf", "report.pdf");
533+
534+
// Assert — revoke fires with the same URL that was assigned to href
535+
expect(window.URL.revokeObjectURL).toHaveBeenCalledTimes(1);
536+
expect(window.URL.revokeObjectURL).toHaveBeenCalledWith(
537+
"blob:http://localhost/fake-object-url",
538+
);
539+
});
517540
});
518541

519542
describe("previewRequest", () => {

0 commit comments

Comments
 (0)