Skip to content

fix(res): remove magic from res.set() Content-Type - #7430

Open
vaibhavmashal wants to merge 2 commits into
expressjs:masterfrom
vaibhavmashal:master
Open

fix(res): remove magic from res.set() Content-Type#7430
vaibhavmashal wants to merge 2 commits into
expressjs:masterfrom
vaibhavmashal:master

Conversation

@vaibhavmashal

Copy link
Copy Markdown

Problem

Currently, res.set() contains unexpected magic when setting the Content-Type header. If a given content-type string does not contain a / (e.g., shorthand like 'json' or invalid input), res.set() attempts a MIME type lookup via mime.contentType(). This mutates user input unexpectedly and causes a known bug (Issue #7034), where unrecognized types return false from mime.contentType() and the header gets coercively set to the literal string "false".

Solution

This PR removes the MIME type lookup logic from res.set() completely, keeping res.set() strictly for assigning header values as provided, which resolves unexpected mutations. The intended way to set a content type with MIME type expansion remains res.type() (which already exists for this exact purpose).

By migrating internal dependencies (res.json and res.jsonp) to use res.type() instead of res.set(), we maintain backward compatibility for built-in response types (such as automatically appending charset=utf-8 to JSON responses) while fulfilling the goal of making res.set() predictable.

Changes Made

  • lib/response.js:
    • res.set(): Removed the mime.contentType() assignment block and the associated obsolete JSDoc comments. Kept the validation that prevents Content-Type from being set to an Array.
    • res.json(): Refactored this.set('Content-Type', ...) to this.type('json') to preserve charset=utf-8 expansion.
    • res.jsonp(): Refactored this.set('Content-Type', ...) to this.type('json') and this.type('text/javascript') to maintain expansion behavior for JSON/JSONP responses.
  • test/res.send.js:
    • Updated expectations in tests where res.set('Content-Type', 'text/plain') was used, expecting exactly 'text/plain' rather than 'text/plain; charset=utf-8'.

Testing

  • To reproduce the original bug:
    1. Call res.set('Content-Type', 'some-custom-type');
    2. Observe that the header sent in the response is erroneously set to Content-Type: false.
  • To verify the fix:
    1. Run the same test above, and observe the output correctly maintains Content-Type: some-custom-type.
    2. Verify that res.type('json') and res.json({}) still properly resolve to application/json; charset=utf-8.
    3. Ensure all tests in the Express suite pass via npm test (all 1260 tests passed locally).

Fixes #7145
Fixes #7034

Copilot AI lite review requested due to automatic review settings August 25, 2026 16:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@kilisamemarisaaa kilisamemarisaaa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Removing the Content-Type expansion here also changes res.format(). Unlike res.json() and res.jsonp(), its internal call still uses:

this.set('Content-Type', normalizeType(key).value)

I reproduced this with a formatter that ends the response directly:

res.format({
  'text/plain': () => res.end('ok')
})

On the current base (023767fe) the response is text/plain; charset=utf-8; on this head (2151c645) it is only text/plain. The existing res.format suite still passes 38/38 because its handlers use res.send(), which adds the charset later and masks the regression.

Could res.format() be migrated to the explicit type API as well, with a regression test whose formatter uses res.end()?

cultosagent added a commit to cultosagent/dogma-registry that referenced this pull request Aug 31, 2026
cultosagent added a commit to cultosagent/dogma-registry that referenced this pull request Aug 31, 2026
@vaibhavmashal

Copy link
Copy Markdown
Author

Thanks for the great catch @kilisamemarisaaa!

I've updated
es.format()\ to use \ his.type()\ instead of direct \ his.set('Content-Type'), and updated
es.type()\ to ensure mime types containing /\ are also resolved with charset expansion via \mime.contentType(type) || type.

I also added the regression test in \ est/res.format.js\ asserting that a formatter calling
es.end()\ directly gets the expected \Content-Type: text/plain; charset=utf-8.

Pushed in commit \5ab54bc5\ — all format and response tests are passing cleanly.

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.

Remove magic from res.set res.set('Content-Type') silently sets header to literal string 'false' for unknown types

3 participants