Skip to content

bug(downloadCSV): crashes in SSR — uses document, URL.createObjectURL, document.body.appendChild without browser guard #479

@roshinit-a

Description

@roshinit-a

Description

downloadCSV() in web-components/src/utils/index.ts uses four browser-only APIs with no SSR guard:

const link = document.createElement("a")         // ❌ ReferenceError in Node.js
const url = URL.createObjectURL(blob)             // ❌ Not available in Node.js
document.body.appendChild(link)                   // ❌ ReferenceError in Node.js
link.click()                                      // ❌ ReferenceError in Node.js
document.body.removeChild(link)                   // ❌ ReferenceError in Node.js
URL.revokeObjectURL(url)                          // ❌ Not available in Node.js

When these web components are consumed in an SSR environment (SvelteKit, Nuxt, server middleware), importing any component that triggers downloadCSV at module evaluation time — or calling it during SSR rendering — throws:

ReferenceError: document is not defined

Note that a similar issue was already fixed in the same file for createDebounce (which used window.setTimeout instead of plain setTimeout). This PR applies the same SSR-awareness to downloadCSV.

Fix

Add a typeof document === "undefined" guard at the top of downloadCSV():

 export const downloadCSV = (rows: Array<Array<any>>, filename: string, headers: Array<string>) => {
   if (rows.length === 0) {
     return
   }

+  if (typeof document === "undefined" || typeof URL === "undefined") {
+    console.warn("downloadCSV: browser APIs not available (SSR environment), skipping download.")
+    return
+  }
+
   const csvContent = [headers.join(","), ...rows.map((row) => row.join(","))].join("\n")
   ...

Also adds a unit test to the existing describe("downloadCSV") block in index.test.ts to verify the guard fires correctly in a simulated SSR environment.

Checklist

  • I have identified the root cause and the minimal fix
  • Fix is consistent with the existing pattern used for createDebounce in the same file
  • No new dependencies required

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions