import { createReadableStreamFromReadable } from '@react-router/node';
import { type createReadableStreamFromReadable as RRCreateReadableStreamFromReadable } from '@react-router/node';
import {
type FastifyReply,
type FastifyRequest,
type RouteGenericInterface,
} from 'fastify';
import type * as http from 'node:http';
import type * as http2 from 'node:http2';
import type * as https from 'node:https';
import { Readable } from 'node:stream';
import {
type AppLoadContext,
createRequestHandler,
type ServerBuild,
} from 'react-router';
type GenericGetLoadContextFunction<
Server extends HttpServer,
AppLoadContext,
> = (
request: FastifyRequest<RouteGenericInterface, Server>,
reply: FastifyReply<RouteGenericInterface, Server>,
) => AppLoadContext | Promise<AppLoadContext>;
type GetLoadContextFunction<Server extends HttpServer = HttpServer> =
GenericGetLoadContextFunction<Server, AppLoadContext>;
type HttpServer =
| http2.Http2SecureServer
| http2.Http2Server
| http.Server
| https.Server;
type RequestHandler<Server extends HttpServer> = (
request: FastifyRequest<RouteGenericInterface, Server>,
reply: FastifyReply<RouteGenericInterface, Server>,
) => Promise<void>;
export function createReactRouterRequestHandler<Server extends HttpServer>({
build,
getLoadContext,
// eslint-disable-next-line n/no-process-env
mode = process.env.NODE_ENV,
}: {
build: (() => Promise<ServerBuild> | ServerBuild) | ServerBuild;
getLoadContext?: GetLoadContextFunction<Server>;
mode?: string;
}): RequestHandler<Server> {
const handleRequest = createRequestHandler(build, mode);
return async (request, reply) => {
const remixRequest = createReactRouterRequest(request, reply);
const loadContext = await getLoadContext?.(request, reply);
const response = await handleRequest(remixRequest, loadContext);
return sendResponse(reply, response);
};
}
function createHeaders(requestHeaders: FastifyRequest['headers']): Headers {
const headers = new Headers();
for (const [key, values] of Object.entries(requestHeaders)) {
if (values) {
if (Array.isArray(values)) {
for (const value of values) {
headers.append(key, value);
}
} else {
headers.set(key, values);
}
}
}
return headers;
}
function createReactRouterRequest<Server extends HttpServer>(
request: FastifyRequest<RouteGenericInterface, Server>,
reply: FastifyReply<RouteGenericInterface, Server>,
): Request {
return createRequest(request, reply, createReadableStreamFromReadable);
}
function createRequest<Server extends HttpServer>(
request: FastifyRequest<RouteGenericInterface, Server>,
reply: FastifyReply<RouteGenericInterface, Server>,
createReadableStreamFromReadable: typeof RRCreateReadableStreamFromReadable,
): Request {
const url = getUrl(request);
let controller: AbortController | null = new AbortController();
const init: RequestInit = {
headers: createHeaders(request.headers),
method: request.method,
signal: controller.signal,
};
// Abort action/loaders once we can no longer write a response if we have
// not yet sent a response (i.e., `close` without `finish`)
// `finish` -> done rendering the response
// `close` -> response can no longer be written to
reply.raw.on('finish', () => (controller = null));
reply.raw.on('close', () => controller?.abort());
if (request.method !== 'GET' && request.method !== 'HEAD') {
init.body = createReadableStreamFromReadable(request.raw);
init.duplex = 'half';
}
return new Request(url, init);
}
function getUrl<Server extends HttpServer>(
request: FastifyRequest<RouteGenericInterface, Server>,
): string {
const origin = `${request.protocol}://${request.host}`;
// Use `request.originalUrl` so Remix and React Router are aware of the full path
const url = `${origin}${request.originalUrl}`;
return url;
}
function responseToReadable(response: Response): null | Readable {
if (!response.body) return null;
const reader = response.body.getReader();
const readable = new Readable();
readable._read = async () => {
const result = await reader.read();
if (result.done) {
readable.push(null);
} else {
readable.push(Buffer.from(result.value));
}
};
return readable;
}
async function sendResponse<Server extends HttpServer>(
reply: FastifyReply<RouteGenericInterface, Server>,
nodeResponse: Response,
): Promise<void> {
reply.status(nodeResponse.status);
for (const [key, values] of nodeResponse.headers.entries()) {
reply.headers({ [key]: values });
}
if (nodeResponse.body) {
const stream = responseToReadable(nodeResponse.clone());
return reply.send(stream);
}
return reply.send(await nodeResponse.text());
}