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