Skip to content

feat(uploads): headersSent guard on stream errors + text/html MIME for GridFS#3745

Merged
PierreBrisorgueil merged 2 commits into
masterfrom
feat/uploads-headerssent-and-html-mime
May 31, 2026
Merged

feat(uploads): headersSent guard on stream errors + text/html MIME for GridFS#3745
PierreBrisorgueil merged 2 commits into
masterfrom
feat/uploads-headerssent-and-html-mime

Conversation

@PierreBrisorgueil
Copy link
Copy Markdown
Contributor

@PierreBrisorgueil PierreBrisorgueil commented May 31, 2026

Closes #3744

Summary

  • headersSent guard (crash fix): get() and getSharp() stream error handlers now check res.headersSent before attempting to write a 422 response. If GridFS errors mid-transfer — after res.set('Content-Type', ...) has already flushed the response head — the handler calls logger.error(...) + res.destroy(err) instead of double-sending, which would throw ERR_HTTP_HEADERS_SENT and crash the worker process. Pre-header errors still return 422 as before. Defense-in-depth adopted from trawl hardening; every devkit downstream gets it via /update-stack.
  • text/html MIME entry: 'text/html': 'html' added to MIME_TO_EXT in uploads.service.js so downstream features that store rendered HTML snapshots in GridFS (e.g. error page captures, scrap previews) get correct .html filenames without requiring per-deployment config.uploads.mimeTypes overrides. text/html is intentionally absent from SAFE_MIME, so get() still downgrades it to application/octet-stream + Content-Disposition: attachment — the stored-XSS defense is fully intact.
  • Tests: headersSent guard tests ported and generalized (4 new cases in uploads.controller.contenttype.unit.tests.js); MIME map test for built-in text/html entry added to uploads.createFromBuffer.unit.tests.js. Full unit suite: 1622/1622 pass.

Files changed

  • modules/uploads/controllers/uploads.controller.js — add logger import, headersSent guard in get() + getSharp()
  • modules/uploads/services/uploads.service.js — add 'text/html': 'html' to MIME_TO_EXT
  • modules/uploads/tests/uploads.controller.contenttype.unit.tests.js — add mockLogger mock + 4 headersSent guard tests
  • modules/uploads/tests/uploads.createFromBuffer.unit.tests.js — add text/html MIME map test

Invariants preserved

  • Devkit's SAFE_MIME / SAFE_IMAGE_MIME / normalizeMime / Content-Disposition: attachment are all untouched
  • text/html not in SAFE_MIME → still served as application/octet-stream with attachment disposition
  • Pre-header stream errors still return 422 to clients

References

Trawl → Devkit alignment plan: docs/superpowers/plans/2026-05-30-trawl-devkit-perfect-alignment.md Task C.3

Summary by CodeRabbit

  • Bug Fixes

    • Improved file upload stream error handling to prevent crashes in edge case scenarios.
  • New Features

    • Added support for HTML file uploads with proper file extension assignment.

…r GridFS (#3744)

Add res.headersSent check in get() and getSharp() stream error handlers so
a GridFS failure mid-transfer (after headers are already flushed) destroys
the socket via res.destroy(err) + logger.error instead of attempting a
second status+body write that throws ERR_HTTP_HEADERS_SENT. Pre-header
errors still return 422 as before.

Add 'text/html': 'html' to MIME_TO_EXT so downstream features storing
rendered HTML snapshots in GridFS get correct .html filenames without
per-deployment config overrides. text/html is intentionally absent from
SAFE_MIME, so the Content-Type allowlist + Content-Disposition: attachment
XSS defenses remain intact.

Port stream-error tests for the headersSent guard and add a MIME map test
for text/html. Full unit suite: 1622/1622 pass.

Adopted from trawl prod hardening; every devkit downstream gets it via
/update-stack.
Copilot AI review requested due to automatic review settings May 31, 2026 18:27
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

Warning

Review limit reached

@PierreBrisorgueil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 18 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 12ab851b-a1dd-48ee-bcd5-154b33ec548a

📥 Commits

Reviewing files that changed from the base of the PR and between a50c4f3 and ba01e84.

📒 Files selected for processing (2)
  • modules/uploads/controllers/uploads.controller.js
  • modules/uploads/tests/uploads.controller.contenttype.unit.tests.js

Walkthrough

This PR hardens the uploads module with two complementary changes: a res.headersSent guard in stream error handlers to prevent ERR_HTTP_HEADERS_SENT crashes after response headers flush, and a built-in text/html MIME-to-extension mapping to support HTML GridFS uploads without deployment-specific config overrides.

Changes

Uploads module hardening: stream error guards and text/html MIME support

Layer / File(s) Summary
Stream error handling in controllers
modules/uploads/controllers/uploads.controller.js
Logger is imported; get and getSharp stream error handlers now conditionally respond based on res.headersSent: pre-header errors trigger responses.error(res, 422, ...) as before; post-header errors log via logger.error and destroy the socket with res.destroy(err).
Test coverage for stream error scenarios
modules/uploads/tests/uploads.controller.contenttype.unit.tests.js
Logger is mocked in suite setup; new test cases for get and getSharp simulate stream failures via captured error handlers and verify res.headersSent-gated behavior: pre-header routes to responses.error, post-header suppresses responses.error, calls res.destroy(err), and logs uploads.get / uploads.getSharp errors.
Built-in text/html MIME type support
modules/uploads/services/uploads.service.js, modules/uploads/tests/uploads.createFromBuffer.unit.tests.js
MIME_TO_EXT mapping adds text/html: 'html' entry; test verifies createFromBuffer resolves text/html MIME type to .html filename extension via built-in mapping without relying on per-deployment config.uploads.mimeTypes overrides.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #3736: Tests were failing because the logger import in the controller needs to be mocked before the controller module is imported; this PR adds the mock logger setup and assertions, resolving that test dependency.

Possibly related PRs

  • pierreb-devkit/Node#3290: Both PRs extend the MIME_TO_EXT filename-extension mapping in createFromBuffer for GridFS buffer uploads; the retrieved PR introduced the mapping, this PR adds the text/html entry.

  • pierreb-devkit/Node#3408: Both PRs modify modules/uploads/services/uploads.service.js to handle text/html MIME type mapping; the retrieved PR made the map extensible via config, this PR hardcodes the built-in entry.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes both main changes: headersSent guard for stream error handling and text/html MIME support, matching the actual code modifications.
Description check ✅ Passed The description covers all required template sections: clear summary with context, scope identifying modules and risk level (medium), validation checklist items, guardrails compliance, and detailed notes for reviewers including security and invariants.
Linked Issues check ✅ Passed All objectives from issue #3744 are met: headersSent guard implemented in stream handlers [#3744], text/html MIME entry added to MIME_TO_EXT [#3744], security invariants preserved with text/html excluded from SAFE_MIME [#3744], and unit tests added for both features [#3744].
Out of Scope Changes check ✅ Passed All changes directly address issue #3744 requirements: headersSent guard, text/html MIME mapping, logger import, and corresponding tests. No extraneous modifications to unrelated code or files detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/uploads-headerssent-and-html-mime

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the uploads download endpoints against mid-stream GridFS failures that can otherwise trigger ERR_HTTP_HEADERS_SENT, and extends the built-in MIME→extension mapping so text/html buffers stored in GridFS get .html filenames by default.

Changes:

  • Add res.headersSent guard in get() and getSharp() stream error handlers to avoid double-sending responses; log + destroy the response when headers are already sent.
  • Add 'text/html': 'html' to the built-in MIME_TO_EXT map used by createFromBuffer().
  • Add/extend unit tests to cover the new headers-sent behavior and the built-in text/html mapping.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
modules/uploads/controllers/uploads.controller.js Add logger + headersSent guard to prevent ERR_HTTP_HEADERS_SENT crashes during streaming.
modules/uploads/services/uploads.service.js Add built-in text/htmlhtml mapping for generated filenames.
modules/uploads/tests/uploads.controller.contenttype.unit.tests.js Add logger mocking and new unit tests for pre/post-headers stream error behavior.
modules/uploads/tests/uploads.createFromBuffer.unit.tests.js Add unit test verifying text/html resolves to .html without config overrides.

Comment thread modules/uploads/controllers/uploads.controller.js Outdated
Comment thread modules/uploads/controllers/uploads.controller.js Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 31, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.03%. Comparing base (0693e49) to head (ba01e84).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3745      +/-   ##
==========================================
+ Coverage   89.96%   90.03%   +0.06%     
==========================================
  Files         148      148              
  Lines        4885     4898      +13     
  Branches     1542     1546       +4     
==========================================
+ Hits         4395     4410      +15     
+ Misses        385      383       -2     
  Partials      105      105              
Flag Coverage Δ
integration 60.00% <66.66%> (-0.06%) ⬇️
unit 68.49% <70.00%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0693e49...ba01e84. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 31, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/uploads/controllers/uploads.controller.js`:
- Around line 42-43: The null-stream check in the getStream handler currently
calls responses.error(res, 404, ...)() but does not return, so execution
continues and dereferences stream (stream.on(...)) causing a TypeError and later
ERR_HTTP_HEADERS_SENT; update the getStream block to immediately return after
calling responses.error(res, 404, ...)(), and apply the same fix to the getSharp
path (the falsy sharp/stream check) so both handlers exit early when the
resource is missing.
- Around line 80-92: getSharp’s current error handler only listens on the
GridFS/source stream; attach the same headersSent-gated handler to the sharp
Transform (or replace inline piping with stream.pipeline) so sharp() transform
errors are caught. Locate the code that calls stream.pipe(sharp(...)).pipe(res)
(the sharp() Transform) and either: 1) create a const transform = sharp(...);
add transform.on('error', (err) => { if (!res.headersSent) {
responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err);
} else { logger.error('uploads.getSharp - sharp transform error after headers
sent', err); res.destroy(err); } }); and then pipe stream -> transform -> res;
or 2) use stream.pipeline(stream, sharp(...), res, (err) => { if (err) { if
(!res.headersSent) { responses.error(res, 422, 'Unprocessable Entity',
errors.getMessage(err))(err); } else { logger.error('uploads.getSharp - pipeline
error after headers sent', err); res.destroy(err); } } }); Ensure references to
responses.error, logger.error and res.destroy remain the same for consistent
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a9439798-f7f8-40d2-beb6-775ae31cf2ca

📥 Commits

Reviewing files that changed from the base of the PR and between 0693e49 and a50c4f3.

📒 Files selected for processing (4)
  • modules/uploads/controllers/uploads.controller.js
  • modules/uploads/services/uploads.service.js
  • modules/uploads/tests/uploads.controller.contenttype.unit.tests.js
  • modules/uploads/tests/uploads.createFromBuffer.unit.tests.js

Comment thread modules/uploads/controllers/uploads.controller.js Outdated
Comment thread modules/uploads/controllers/uploads.controller.js Outdated
…rrors

Address two CodeRabbit findings:

1. Add return before responses.error(res, 404) in both get() and getSharp()
   so execution does not fall through to stream.on() when getStream returns
   falsy — preventing a TypeError and ERR_HTTP_HEADERS_SENT crash on the
   null-stream path.

2. Attach the headersSent-gated error handler to the sharp Transform (not
   just the GridFS source stream). pipe() does not forward error events
   between streams, so sharp-pipeline failures were unhandled and could
   crash the worker process. Refactored to buildTransform() helper that
   creates the transform, attaches the error handler, and returns it.
   Update comment to accurately describe when headersSent flips to true
   (first chunk written, not res.set()).

Add test: sharp transform error after headers sent → res.destroy + logger.
Full unit suite: 1623/1623 pass.
@PierreBrisorgueil PierreBrisorgueil dismissed coderabbitai[bot]’s stale review May 31, 2026 18:37

Both findings addressed in follow-up commit: added return on null-stream 404 paths (get + getSharp) and added error handler on sharp Transform via buildTransform() helper. Comments are on outdated diff.

@PierreBrisorgueil PierreBrisorgueil merged commit 2ff4161 into master May 31, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(uploads): headersSent guard on stream errors + allow text/html MIME for GridFS snapshots

2 participants