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