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