Kraken JS Content Negotiation Per-Route API Format Selection
When Accept headers cause unexpected Kraken API responses, explicit renderer registration and route-level negotiation prevent 406 errors and format mismatches.
21 Jan 2026, 20:38 UTC

The Problem: Accept Headers That Go Unhandled
You configure a Kraken API endpoint expecting JSON responses when a client sends Accept: application/json. Instead, the response arrives as HTML, or the server returns 406 Not Acceptable. This happens because Kraken’s content negotiation relies on registered renderers—if no renderer matches the declared Accept header, the framework either defaults to an unexpected format or rejects the request entirely.
How Kraken’s Response Layer Negotiates Format
Kraken integrates Express’s req.accepts() method with its own response layer. When a request arrives, Kraken inspects the Accept header and attempts to match it against registered renderers. The framework ships with built-in renderers for application/json and text/html.
Developers can register custom renderers for XML, CSV, or any mime type. Registration typically happens in the application bootstrap, often environment-specific:
// config.json or app bootstrap
const renderers = {
'application/json': require('your-renderer'),
'text/html': require('your-html-renderer')
};
app.res.registerRenderers(renderers);
\nRoute-level configuration
Kraken allows per-endpoint Accept header handling, which is useful for API versioning or format-specific payloads without global side effects. You can attach negotiation logic in a route handler or middleware:
router.get('/users', (req, res, next) => {
// Negotiate format based on Accept header
const acceptable = req.accepts('application/json', 'text/html');
if (acceptable === 'application/json') {
res.json(users);
} else {
res.html(users);
}
});
\nWorking Example: Per-Route JSON Negotiation
Suppose you want an endpoint that returns JSON when the client signals preference, and falls back to HTML only if explicitly requested, without relying on Kraken’s default renderer cascade.
\nStep 1: Register desired renderers in the appropriate environment config
Edit config/env/production.js (or development.js) to ensure the JSON renderer is active:
// config/env/production.js
module.exports = function(deps) {
const app = deps.app;
// Register JSON renderer explicitly for production
app.res.registerRenderer('application/json', deps.jsonRenderer);
// Optionally register HTML only in development
if (process.env.NODE_ENV !== 'production') {
app.res.registerRenderer('text/html', deps.htmlRenderer);
}
};
\nStep 2: Use req.accepts() in your route
router.get('/api/users', (req, res) => {
// Check if client prefers JSON; returns the best match or false
const format = req.accepts('application/json');
if (format === 'application/json') {
res.json({ data: 'example' });
} else if (req.accepts('text/html')) {
res.send('...');
} else {
res.status(406).send('Not acceptable');
}
});
\nStep 3: Verify with curl
Run the following commands from your project root (no special permissions required beyond node access):
curl -H 'Accept: application/json' http://localhost:8080/api/users— should return JSON if the renderer is registered andreq.accepts()matches.curl -H 'Accept: text/html' http://localhost:8080/api/users— should return HTML if the HTML renderer is registered and active.curl -H 'Accept: application/xml' http://localhost:8080/api/users— will likely return406if no XML renderer is registered, which is expected.
After each request, check the response body and HTTP status. If you see 406 when you expect JSON, confirm that the JSON renderer is registered in the current NODE_ENV and that no middleware is stripping or modifying the Accept header before it reaches your route.
Trade-offs, Verification, and the 406 Trap
Content negotiation in Kraken is powerful but has pitfalls. Middleware that runs before your route and modifies req.accepts—such as body parsers that consume the Accept header or custom logging—can cause the method to return unexpected results. Additionally, over-relying on Kraken’s default renderers without explicit registration may hide missing format handling, especially in API-first projects where you want strict format control.
To verify your setup practically:
- Inspect registered renderers:
console.log(Object.keys(app.res.renderers))in a Node REPL or app bootstrap. The output should list the mime types you registered. - Send requests with varied
Acceptheaders usingcurlas shown above. - Check your environment-specific config files (
config/env/*.js) to confirm renderer sets differ between development and production as intended.
If you receive 406 Not Acceptable unexpectedly, first check whether any upstream middleware is altering the Accept header, and second confirm the renderer for the desired format is registered in the active environment.
Actionable Closing
Start small: add one explicit renderer registration to your Kraken app’s environment config, then test a single endpoint with different Accept headers using curl. Once you’ve confirmed the format switches as expected, gradually introduce per-route req.accepts() logic for the level of control your API requires. The framework’s content negotiation is designed to be opt-in, not implicit—making your renderer choices explicit is the safest path to reliable API responses.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.