fix: merge Fastify headers with Remix headers instead of overriding them
Headers set by Fastify before calling the Remix handler were being overridden by Remix response headers instead of merged. This broke use cases like adding Link preload headers in Fastify middleware.
// Before: Fastify's Link header was lost
instance.all('*', async (request, reply) => {
reply.header('Link', '</fonts/my-font.woff2>; rel=preload; as=font');
return handler(request, reply); // Remix headers override Fastify's
});
// After: Headers are merged correctly
// Final response contains both Fastify and Remix Link headers
Changes
-
sendResponseinshared.ts: Collects response headers first, then merges with existing Fastify reply headers instead of replacing them -
Header splitting: Handles Web Headers API behavior where multi-value headers like
Linkare combined with ", " (exceptSet-Cookiewhich remains separate) - Test coverage: Added test verifying Link headers from both Fastify and Remix appear in final response
Technical notes
The Web Headers API is lossy - it combines multi-value headers with ", " separators. We split on ", " to enable proper merging, which works correctly for Link headers but may incorrectly split headers containing literal commas in their values. This is an acceptable tradeoff given the Headers API provides no alternative.
Original prompt
This section details on the original issue you should resolve
<issue_title>Merge headers</issue_title> <issue_description>These seems to be an issue, where if I send headers using Fastify, then anything that gets added by Remix is overriden, e.g.
Here I am adding
linkheader:await app.register(async (instance) => { instance.decorateRequest('cspNonce', null); instance.removeAllContentTypeParsers(); instance.addContentTypeParser('*', (_request, payload, done) => { done(null, payload); }); const handler = createRequestHandler({ build: serverBuild, getLoadContext(request) { return { nonce: request.cspNonce, session: request.session as unknown as Session, visitor: request.visitor, }; }, mode: 'production', }); instance.all('*', async (request, reply) => { const links = getLinks(request.url); reply.header( 'Link', [ '</fonts/gabarito/Gabarito-VariableFont.woff2>; rel=preload; as=font; crossorigin', ...links, ].join(', '), ); try { return handler(request, reply); } catch (error) { captureException({ error, extra: { url: request.url, }, message: 'could not render request', }); return replyWithErrorPage(reply, error); } }); });but the loaded route also send
linkheader, which overrides my value from the above.I would expect headers such as
linkto be merged.</issue_description>Comments on the Issue (you are @copilot in this section)
@mcansh hey @lilouartz thanks for info i'll poke around soon@mcansh `0.0.0-experimental-1d46df2` looks like there's some edge cases where headers are added multiple times as im not limiting it to just `Link` headers, but should be good for a test :)
- Fixes mcansh/remix-fastify#405
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.