Skip to content

fix: stop the compliance panel crashing on a clean period, and leaking a blob per download (#1138) - #1143

Merged
Aditya8369 merged 1 commit into
Aditya8369:mainfrom
MOHITKOURAV01:fix/1138-compliance-report-download
Sep 1, 2026
Merged

fix: stop the compliance panel crashing on a clean period, and leaking a blob per download (#1138)#1143
Aditya8369 merged 1 commit into
Aditya8369:mainfrom
MOHITKOURAV01:fix/1138-compliance-report-download

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

Description

Four defects in the Regulatory Compliance Reporting generate-then-download path. Together
they mean a clean compliance period crashes the page and every download leaks memory.

1. A report with no exceedances crashes the panel. exceedances is read unguarded by
both the table (report.exceedances.length) and exportToCSV (report.exceedances.map),
but it is optional over the wire — a compliant period is reasonably returned as
{ id, standard, totalExceedances: 0 } with no list at all. The component even has a
"No exceedances found in this period. Good job!" empty row for exactly that case, and
reaching it gave TypeError: Cannot read properties of undefined (reading 'length'). The
one outcome this feature exists to report was the one that broke it.

2. Every download leaked an object URL. Neither branch of handleDownload called
revokeObjectURL, so each export pinned its blob in memory until the tab closed. The
anchor was also never attached to the document before the click, which some browsers ignore
outright.

3. The error path replaced the server's message with a parser error.

if (!response.ok) {
    const errorData = await response.json();               // throws on a non-JSON body
    throw new Error(errorData.message || 'Failed to generate report');   // unreachable
}

A 502 from a proxy, an HTML error page or an empty 500 body all reject with
SyntaxError: Unexpected token '<', "<html>"... — and that is the string the user saw.

4. No date-range validation, and the filename carried only the start date, so two
reports for different periods sharing a start date overwrote each other in the downloads
folder.


Related Issue

Closes #1138


Type of Change

  • Bug Fix
  • New Feature
  • Documentation
  • UI/UX Improvement
  • Refactoring
  • Performance Improvement
  • Accessibility

Changes Made

src/utils/downloadFile.js (new)

downloadFile(content, mimeType, filename) — attaches the anchor before clicking it,
removes it in a finally, and revokes the object URL on the next tick (revoking
synchronously cancels the download in browsers that read the blob after the click returns;
never revoking leaks).

This is not new code so much as relocated code: chartExport.js already had it right and
kept it private. chartExport.js now imports it, so the correct version is in one place
instead of being copied a fourth time.

safeFilenamePart sanitises the parts of a filename that come from form input.

src/services/complianceEngine.js

  • normaliseReport() guarantees exceedances is an array and derives totalExceedances
    from the list only when the server didn't send a count (the list can be a truncated page
    of a larger total, so the server's number wins). Exported, because the component needs
    the same guarantee for a report it already holds.
  • describeFailure() reads the error body in a try, so a non-JSON body falls back to
    Failed to generate report (HTTP 502) instead of a SyntaxError.
  • The auth header is omitted rather than sent as Bearer null, and the token read is
    guarded — blocked site data makes the localStorage access itself throw, and a 401 is a
    much better failure than a SecurityError out of a service call.
  • downloadComplianceReport — imported by the component and never called — goes through
    the shared helper, and encodes the report id and format into its URL.
  • Both take an optional AbortSignal.

src/components/ComplianceReportGenerator.jsx

  • Renders from normaliseReport(report), so the missing-exceedances shape reaches
    neither the table nor the exporter.
  • describeRangeProblem() and reportFilename() — exported and pure, so the rules are
    testable without rendering. The filename now carries both ends of the period.
  • Validation messages land in a role="alert", and the form is noValidate: the native
    bubble is not reliably announced and vanishes on the next interaction, so one announced
    message for every failure is better here. The required attributes stay for the
    aria-required they imply.
  • The three form labels are tied to their inputs with htmlFor/id. That also clears the
    three jsx-a11y/label-has-associated-control warnings on this file.
  • The in-flight request is aborted on unmount.
  • The stat grid declares md:grid-cols-4 and only ever had two tiles in it. The period and
    the generation time were already on the report and nowhere on screen — they fill it.

Tests (new)downloadFile.test.js (11), complianceEngine.test.js (19),
ComplianceReportGenerator.test.jsx (18).


Testing

  • Tested locally
  • No console errors
  • Existing functionality works as expected
$ npx vitest run src/utils/downloadFile.test.js src/services/complianceEngine.test.js \
                src/components/ComplianceReportGenerator.test.jsx src/utils/chartExport.test.js
 ✓ src/utils/downloadFile.test.js (11 tests)
 ✓ src/services/complianceEngine.test.js (19 tests)
 ✓ src/components/ComplianceReportGenerator.test.jsx (18 tests)
 ✓ src/utils/chartExport.test.js (8 tests)
 Test Files  4 passed (4)
      Tests  56 passed (56)

The 8 existing chartExport tests still pass against the extracted helper — that's the
check that the move didn't change its behaviour. Reverting the two source files and
re-running the new suites fails 16 tests, including the two that reproduce the
TypeError and the SyntaxError.

Deliberately not touched

src/utils/reportExporter.js — the CSV escaping there is #1052, and #1057 is already open
against that file. Keeping off it means these two can merge in either order.

Note on CI

Lint, Build and Playwright are red on main and on every open PR — npm run build fails
on the 3 parse errors of #1129 (App.jsx, Leaderboard.jsx, NoisePollutionTracker.jsx).
Nothing here touches those files.

…g a blob per download (Aditya8369#1138)

Four defects in the generate-then-download path.

1. `exceedances` is read unguarded by both the table and exportToCSV, but it is
   optional over the wire -- a compliant period is reasonably returned as
   `{ totalExceedances: 0 }` with no list at all. The component even has a "No
   exceedances found in this period. Good job!" row for that case, and reaching
   it threw `TypeError: Cannot read properties of undefined (reading 'length')`.
   The one outcome the feature exists to report was the one that broke it.
   normaliseReport() now guarantees the array at the service boundary.

2. Neither download branch revoked its object URL, so every export pinned its
   blob for the life of the document, and the anchor was never attached before
   the click -- which some browsers ignore outright. chartExport.js already had
   this right in a private helper; it moves to src/utils/downloadFile.js and
   chartExport now imports it rather than the code being copied a third time.
   downloadComplianceReport, imported by the component and never called, uses it
   too.

3. The error path called `await response.json()` unguarded, which throws on a
   502 from a proxy, an HTML error page or an empty body. The
   `|| 'Failed to generate report'` fallback on the next line could never run,
   because the line before it was what threw, and the user was shown
   `SyntaxError: Unexpected token '<'`.

4. The form only checked that both dates were present, so an end date before the
   start was submitted happily, and the filename carried only the start date --
   two reports for different periods sharing a start date overwrote each other
   on disk.

Also here, all in files this change already touches: the auth header is omitted
rather than sent as "Bearer null", the token read is guarded so blocked site
data cannot throw a SecurityError out of a service call, the request is aborted
on unmount, the three form labels are tied to their inputs, validation messages
land in a role="alert", and the stat grid declares four columns and now has four
tiles in it instead of two.

Tests: 11 for downloadFile, 19 for complianceEngine, 18 for the panel. Against
the code before this change, 16 of them fail.
@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

@MOHITKOURAV01 is attempting to deploy a commit to the Aditya Mahajan's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

Copy link
Copy Markdown

Thank You for Your Contribution! 🎉

Hi @MOHITKOURAV01,

Thank you for opening this Pull Request and contributing to our project. We truly appreciate your efforts.

Please make sure that:

  • Your code follows the project's guidelines.
  • You have linked the appropriate issue (if applicable).
  • Screenshots are added for UI/UX changes.
  • Your PR is ready for review.

The maintainer @Aditya8369 will review your PR shortly!

Happy Contributing! 🚀

@github-actions github-actions Bot added the ECSoC26 Contributions considered under ECSoC'26 label Aug 29, 2026
@Aditya8369
Aditya8369 merged commit 3d0c4b5 into Aditya8369:main Sep 1, 2026
3 of 13 checks passed
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

🎉 Your PR just got merged, @MOHITKOURAV01 — thank you for contributing to Pollution Control Hub!

Your work is now part of the project. Here's what to do next:

  • ⭐ If you haven't already, consider giving the repo a star — it helps us grow.
  • 📢 Share your contribution on LinkedIn, Twitter, or wherever you hang out. You shipped open source!
  • 🔍 Browse other open issues if you want to keep contributing.

We really appreciate you taking the time. See you in the next PR! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ECSoC26-L3 ECSoC26 Contributions considered under ECSoC'26

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compliance report panel crashes on a period with no exceedances, leaks a blob URL per download, and swallows the server's error message

2 participants