1import { 2 AbortController, 3 Headers, 4 RequestInit, 5 Response, 6 writeReadableStreamToWritable, 7} from '@remix-run/node'; 8import type * as express from 'express'; 9 10import { createRequestHandler as createExpoHandler } from '..'; 11import { ExpoRequest } from '../environment'; 12 13export type RequestHandler = ( 14 req: express.Request, 15 res: express.Response, 16 next: express.NextFunction 17) => Promise<void>; 18 19/** 20 * Returns a request handler for Express that serves the response using Remix. 21 */ 22export function createRequestHandler( 23 { build }: { build: string }, 24 setup?: Parameters<typeof createExpoHandler>[1] 25): RequestHandler { 26 const handleRequest = createExpoHandler(build, setup); 27 28 return async (req: express.Request, res: express.Response, next: express.NextFunction) => { 29 if (!req?.url || !req.method) { 30 return next(); 31 } 32 try { 33 const request = convertRequest(req, res); 34 35 const response = await handleRequest(request); 36 37 await respond(res, response); 38 } catch (error: unknown) { 39 // Express doesn't support async functions, so we have to pass along the 40 // error manually using next(). 41 next(error); 42 } 43 }; 44} 45 46export function convertHeaders(requestHeaders: express.Request['headers']): Headers { 47 const headers = new Headers(); 48 49 for (const [key, values] of Object.entries(requestHeaders)) { 50 if (values) { 51 if (Array.isArray(values)) { 52 for (const value of values) { 53 headers.append(key, value); 54 } 55 } else { 56 headers.set(key, values); 57 } 58 } 59 } 60 61 return headers; 62} 63 64export function convertRequest(req: express.Request, res: express.Response): ExpoRequest { 65 const url = new URL(`${req.protocol}://${req.get('host')}${req.url}`); 66 67 // Abort action/loaders once we can no longer write a response 68 const controller = new AbortController(); 69 res.on('close', () => controller.abort()); 70 71 const init: RequestInit = { 72 method: req.method, 73 headers: convertHeaders(req.headers), 74 // Cast until reason/throwIfAborted added 75 // https://github.com/mysticatea/abort-controller/issues/36 76 signal: controller.signal as RequestInit['signal'], 77 }; 78 79 if (req.method !== 'GET' && req.method !== 'HEAD') { 80 init.body = req; 81 } 82 83 return new ExpoRequest(url.href, init); 84} 85 86export async function respond(res: express.Response, expoRes: Response): Promise<void> { 87 res.statusMessage = expoRes.statusText; 88 res.status(expoRes.status); 89 90 for (const [key, values] of Object.entries(expoRes.headers.raw())) { 91 for (const value of values) { 92 res.append(key, value); 93 } 94 } 95 96 if (expoRes.body) { 97 await writeReadableStreamToWritable(expoRes.body, res); 98 } else { 99 res.end(); 100 } 101} 102