-
-
Notifications
You must be signed in to change notification settings - Fork 54
feat: add contentTypeNegotiation option #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this 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 adds backward-compatible content type negotiation functionality to finalhandler, allowing error responses to be formatted as either text/plain or text/html based on client preferences. The current behavior (always returning text/html) is preserved as the default but can be configured via two new options.
- Added
contentTypeNegotiationoption to enable/disable Accept header-based content negotiation - Added
defaultContentTypeoption to configure the fallback content type - Refactored HTML generation to return Buffers and support both HTML and plain text responses
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| index.js | Core implementation: adds negotiator dependency, implements content type negotiation logic, refactors response body generation to support both HTML and plain text formats |
| test/test.js | Comprehensive test coverage for both new options across various Accept header scenarios for 404 and error responses |
| package.json | Adds negotiator as a new dependency |
| README.md | Documents the two new options with warnings about future default changes |
| HISTORY.md | Records the new features in the unreleased section |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| it('should respond according to quality when contentTypeNegotiation is true', function (done) { | ||
| var server = createServer(null, { contentTypeNegotiation: true }) | ||
| wrapper(request(server) | ||
| .get('/foo')) | ||
| .set('Accept', 'text/html, application/*;q=0.2, image/jpeg;q=0.8') | ||
| .expect('Content-Type', 'text/html; charset=utf-8') | ||
| .expect(404, /<html/, done) | ||
| }) |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two test cases with the identical name "should respond according to quality when contentTypeNegotiation is true" within the same describe block. The second test should have a more specific name that distinguishes it from the first, such as "should prefer text/html when it has higher quality" or "should respond with text/html when quality is higher".
| responses will use `text/plain` or `text/html` based on the client's preferences. Defaults | ||
| to `false`. |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation for contentTypeNegotiation should clarify the behavior when no Accept header is present. Based on the test at line 433, when content negotiation is enabled and there's no Accept header, the response defaults to 'text/plain' (the first item in AVAILABLE_MEDIA_TYPES). This should be explicitly documented to avoid confusion.
| responses will use `text/plain` or `text/html` based on the client's preferences. Defaults | |
| to `false`. | |
| responses will use `text/plain` or `text/html` based on the client's preferences. When | |
| enabled and the request does not include an `Accept` header, the response defaults to | |
| `text/plain` (the first available media type). Defaults to `false`. |
| } | ||
|
|
||
| let preferredType | ||
| // If text/plain fallback is enabled, negotiate content type |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment "If text/plain fallback is enabled" is misleading. The option is named contentTypeNegotiation, not "text/plain fallback", and it enables negotiation between both text/plain and text/html. Consider updating the comment to "If content type negotiation is enabled" for clarity.
| // If text/plain fallback is enabled, negotiate content type | |
| // If content type negotiation is enabled, negotiate content type |
| contentType = HTML_CONTENT_TYPE | ||
| break | ||
| case 'text/plain': | ||
| // default to plain text |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment "default to plain text" is misleading in this context. This case is reached when preferredType is 'text/plain' OR when defaultContentType is 'text/plain' (and no preference was determined). The comment should be removed or clarified to "text/plain response" to avoid confusion.
| // default to plain text | |
| // text/plain response |
| describe('when no Accept header', function () { | ||
| it('should respond with plain text when contentTypeNegotiation is true', function (done) { | ||
| var server = createServer(null, { contentTypeNegotiation: true }) | ||
| wrapper(request(server) | ||
| .get('/foo')) | ||
| .expect('Content-Type', 'text/plain; charset=utf-8') | ||
| .expect(404, 'Cannot GET /foo', done) | ||
| }) | ||
|
|
||
| it('should respond with HTML when contentTypeNegotiation is false', function (done) { | ||
| var server = createServer(null, { contentTypeNegotiation: false }) | ||
| wrapper(request(server) | ||
| .get('/foo')) | ||
| .expect('Content-Type', 'text/html; charset=utf-8') | ||
| .expect(404, /<html/, done) | ||
| }) | ||
|
|
||
| it('should respond with HTML when contentTypeNegotiation is not set', function (done) { | ||
| var server = createServer() | ||
| wrapper(request(server) | ||
| .get('/foo')) | ||
| .expect('Content-Type', 'text/html; charset=utf-8') | ||
| .expect(404, /<html/, done) | ||
| }) | ||
| }) |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no test coverage for the scenario where defaultContentType: 'text/plain' is set but contentTypeNegotiation is false (or not set). According to the implementation at line 154, this should result in plain text responses. Consider adding tests to verify that defaultContentType works independently of contentTypeNegotiation, such as a test that sets only defaultContentType: 'text/plain' and expects plain text responses.
| // get error callback | ||
| var onerror = opts.onerror | ||
|
|
||
| // fallback response content type negotiation enabled |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment "fallback response content type negotiation enabled" is unclear. Consider revising to "content type negotiation enabled" for clarity, as "fallback" is ambiguous in this context.
| // fallback response content type negotiation enabled | |
| // response content type negotiation enabled |
| // default to plain text | ||
| body = Buffer.from(msg, 'utf8') | ||
| contentType = TEXT_CONTENT_TYPE | ||
| break |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The switch statement lacks a default case, which could result in body and contentType being undefined if an unexpected value is used. While the validation at line 91-93 should prevent this, adding a default case would make the code more defensive and prevent potential runtime errors if the validation logic changes in the future.
| break | |
| break | |
| default: | |
| // Defensive fallback: use the configured defaultContentType | |
| if (defaultContentType === 'text/plain') { | |
| body = Buffer.from(msg, 'utf8') | |
| contentType = TEXT_CONTENT_TYPE | |
| } else { | |
| body = createHtmlBody(msg) | |
| contentType = HTML_CONTENT_TYPE | |
| } | |
| break |
| /** | ||
| * Create a minimal HTML document. | ||
| * | ||
| * @param {string} message | ||
| * @private | ||
| */ |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JSDoc comment for this function says "Create a minimal HTML document", but the function has been renamed from createHtmlDocument to createHtmlBody and now returns a Buffer instead of a string. The documentation should be updated to clarify that it returns a Buffer containing the HTML body.
| it('should respond according to quality when contentTypeNegotiation is true', function (done) { | ||
| var server = createServer(createError('boom!'), { contentTypeNegotiation: true }) | ||
| wrapper(request(server) | ||
| .get('/foo')) | ||
| .set('Accept', 'text/html, application/*;q=0.2, image/jpeg;q=0.8') | ||
| .expect('Content-Type', 'text/html; charset=utf-8') | ||
| .expect(500, /<html/, done) | ||
| }) |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two test cases with the identical name "should respond according to quality when contentTypeNegotiation is true" within the same describe block. The second test should have a more specific name that distinguishes it from the first, such as "should prefer text/html when it has higher quality" or "should respond with text/html when quality is higher".
This is a backward-compatible version of #85.
It contains 2 commits:
feat: add contentTypeNegotiation: Adds a
contentTypeNegotiationoption that enables content type negotiation for error responses based on theAcceptheader. When enabled, responses will be formatted as eithertext/plainortext/htmlaccording to client preferences.feat: add defaultContentType option to configure the default response: Adds a
defaultContentTypeoption to configure the default response type when content negotiation is disabled or no preferred type can be determined.The current behaviour (alway returning
text/htmlresponses) is unchanged but the user can configure it if he wants to. I added 2 warning in the README which states that the options default will be changed in the next major.