xref: /expo/packages/@expo/server/src/vendor/netlify.ts (revision d43fb2a3)
1*d43fb2a3SEvan Baconimport type { HandlerEvent, HandlerResponse } from '@netlify/functions';
2*d43fb2a3SEvan Baconimport { Headers, readableStreamToString, RequestInit } from '@remix-run/node';
3*d43fb2a3SEvan Baconimport { AbortController } from 'abort-controller';
4*d43fb2a3SEvan Bacon
5*d43fb2a3SEvan Baconimport { createRequestHandler as createExpoHandler } from '..';
6*d43fb2a3SEvan Baconimport { ExpoRequest, ExpoResponse } from '../environment';
7*d43fb2a3SEvan Bacon
8*d43fb2a3SEvan Baconexport function createRequestHandler({ build }: { build: string }) {
9*d43fb2a3SEvan Bacon  const handleRequest = createExpoHandler(build);
10*d43fb2a3SEvan Bacon
11*d43fb2a3SEvan Bacon  return async (event: HandlerEvent) => {
12*d43fb2a3SEvan Bacon    const response = await handleRequest(convertRequest(event));
13*d43fb2a3SEvan Bacon
14*d43fb2a3SEvan Bacon    return respond(response);
15*d43fb2a3SEvan Bacon  };
16*d43fb2a3SEvan Bacon}
17*d43fb2a3SEvan Bacon
18*d43fb2a3SEvan Baconexport async function respond(res: ExpoResponse): Promise<HandlerResponse> {
19*d43fb2a3SEvan Bacon  const contentType = res.headers.get('Content-Type');
20*d43fb2a3SEvan Bacon  let body: string | undefined;
21*d43fb2a3SEvan Bacon  const isBase64Encoded = isBinaryType(contentType);
22*d43fb2a3SEvan Bacon
23*d43fb2a3SEvan Bacon  if (res.body) {
24*d43fb2a3SEvan Bacon    if (isBase64Encoded) {
25*d43fb2a3SEvan Bacon      body = await readableStreamToString(res.body, 'base64');
26*d43fb2a3SEvan Bacon    } else {
27*d43fb2a3SEvan Bacon      body = await res.text();
28*d43fb2a3SEvan Bacon    }
29*d43fb2a3SEvan Bacon  }
30*d43fb2a3SEvan Bacon
31*d43fb2a3SEvan Bacon  const multiValueHeaders = res.headers.raw();
32*d43fb2a3SEvan Bacon
33*d43fb2a3SEvan Bacon  return {
34*d43fb2a3SEvan Bacon    statusCode: res.status,
35*d43fb2a3SEvan Bacon    multiValueHeaders,
36*d43fb2a3SEvan Bacon    body,
37*d43fb2a3SEvan Bacon    isBase64Encoded,
38*d43fb2a3SEvan Bacon  };
39*d43fb2a3SEvan Bacon}
40*d43fb2a3SEvan Bacon
41*d43fb2a3SEvan Baconexport function createHeaders(requestHeaders: HandlerEvent['multiValueHeaders']): Headers {
42*d43fb2a3SEvan Bacon  const headers = new Headers();
43*d43fb2a3SEvan Bacon
44*d43fb2a3SEvan Bacon  for (const [key, values] of Object.entries(requestHeaders)) {
45*d43fb2a3SEvan Bacon    if (values) {
46*d43fb2a3SEvan Bacon      for (const value of values) {
47*d43fb2a3SEvan Bacon        headers.append(key, value);
48*d43fb2a3SEvan Bacon      }
49*d43fb2a3SEvan Bacon    }
50*d43fb2a3SEvan Bacon  }
51*d43fb2a3SEvan Bacon
52*d43fb2a3SEvan Bacon  return headers;
53*d43fb2a3SEvan Bacon}
54*d43fb2a3SEvan Bacon
55*d43fb2a3SEvan Bacon// `netlify dev` doesn't return the full url in the event.rawUrl, so we need to create it ourselves
56*d43fb2a3SEvan Baconfunction getRawPath(event: HandlerEvent): string {
57*d43fb2a3SEvan Bacon  let rawPath = event.path;
58*d43fb2a3SEvan Bacon  const searchParams = new URLSearchParams();
59*d43fb2a3SEvan Bacon
60*d43fb2a3SEvan Bacon  if (!event.multiValueQueryStringParameters) {
61*d43fb2a3SEvan Bacon    return rawPath;
62*d43fb2a3SEvan Bacon  }
63*d43fb2a3SEvan Bacon
64*d43fb2a3SEvan Bacon  const paramKeys = Object.keys(event.multiValueQueryStringParameters);
65*d43fb2a3SEvan Bacon  for (const key of paramKeys) {
66*d43fb2a3SEvan Bacon    const values = event.multiValueQueryStringParameters[key];
67*d43fb2a3SEvan Bacon    if (!values) continue;
68*d43fb2a3SEvan Bacon    for (const val of values) {
69*d43fb2a3SEvan Bacon      searchParams.append(key, val);
70*d43fb2a3SEvan Bacon    }
71*d43fb2a3SEvan Bacon  }
72*d43fb2a3SEvan Bacon
73*d43fb2a3SEvan Bacon  const rawParams = searchParams.toString();
74*d43fb2a3SEvan Bacon
75*d43fb2a3SEvan Bacon  if (rawParams) rawPath += `?${rawParams}`;
76*d43fb2a3SEvan Bacon
77*d43fb2a3SEvan Bacon  return rawPath;
78*d43fb2a3SEvan Bacon}
79*d43fb2a3SEvan Bacon
80*d43fb2a3SEvan Baconexport function convertRequest(event: HandlerEvent): ExpoRequest {
81*d43fb2a3SEvan Bacon  let url: URL;
82*d43fb2a3SEvan Bacon
83*d43fb2a3SEvan Bacon  if (process.env.NODE_ENV !== 'development') {
84*d43fb2a3SEvan Bacon    url = new URL(event.rawUrl);
85*d43fb2a3SEvan Bacon  } else {
86*d43fb2a3SEvan Bacon    const origin = event.headers.host;
87*d43fb2a3SEvan Bacon    const rawPath = getRawPath(event);
88*d43fb2a3SEvan Bacon    url = new URL(`http://${origin}${rawPath}`);
89*d43fb2a3SEvan Bacon  }
90*d43fb2a3SEvan Bacon
91*d43fb2a3SEvan Bacon  // Note: No current way to abort these for Netlify, but our router expects
92*d43fb2a3SEvan Bacon  // requests to contain a signal so it can detect aborted requests
93*d43fb2a3SEvan Bacon  const controller = new AbortController();
94*d43fb2a3SEvan Bacon
95*d43fb2a3SEvan Bacon  const init: RequestInit = {
96*d43fb2a3SEvan Bacon    method: event.httpMethod,
97*d43fb2a3SEvan Bacon    headers: createHeaders(event.multiValueHeaders),
98*d43fb2a3SEvan Bacon    // Cast until reason/throwIfAborted added
99*d43fb2a3SEvan Bacon    // https://github.com/mysticatea/abort-controller/issues/36
100*d43fb2a3SEvan Bacon    signal: controller.signal as RequestInit['signal'],
101*d43fb2a3SEvan Bacon  };
102*d43fb2a3SEvan Bacon
103*d43fb2a3SEvan Bacon  if (event.httpMethod !== 'GET' && event.httpMethod !== 'HEAD' && event.body) {
104*d43fb2a3SEvan Bacon    const isFormData = event.headers['content-type']?.includes('multipart/form-data');
105*d43fb2a3SEvan Bacon    init.body = event.isBase64Encoded
106*d43fb2a3SEvan Bacon      ? isFormData
107*d43fb2a3SEvan Bacon        ? Buffer.from(event.body, 'base64')
108*d43fb2a3SEvan Bacon        : Buffer.from(event.body, 'base64').toString()
109*d43fb2a3SEvan Bacon      : event.body;
110*d43fb2a3SEvan Bacon  }
111*d43fb2a3SEvan Bacon
112*d43fb2a3SEvan Bacon  return new ExpoRequest(url.href, init);
113*d43fb2a3SEvan Bacon}
114*d43fb2a3SEvan Bacon
115*d43fb2a3SEvan Bacon/**
116*d43fb2a3SEvan Bacon * Common binary MIME types
117*d43fb2a3SEvan Bacon * @see https://github.com/architect/functions/blob/45254fc1936a1794c185aac07e9889b241a2e5c6/src/http/helpers/binary-types.js
118*d43fb2a3SEvan Bacon */
119*d43fb2a3SEvan Baconconst binaryTypes = [
120*d43fb2a3SEvan Bacon  'application/octet-stream',
121*d43fb2a3SEvan Bacon  // Docs
122*d43fb2a3SEvan Bacon  'application/epub+zip',
123*d43fb2a3SEvan Bacon  'application/msword',
124*d43fb2a3SEvan Bacon  'application/pdf',
125*d43fb2a3SEvan Bacon  'application/rtf',
126*d43fb2a3SEvan Bacon  'application/vnd.amazon.ebook',
127*d43fb2a3SEvan Bacon  'application/vnd.ms-excel',
128*d43fb2a3SEvan Bacon  'application/vnd.ms-powerpoint',
129*d43fb2a3SEvan Bacon  'application/vnd.openxmlformats-officedocument.presentationml.presentation',
130*d43fb2a3SEvan Bacon  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
131*d43fb2a3SEvan Bacon  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
132*d43fb2a3SEvan Bacon  // Fonts
133*d43fb2a3SEvan Bacon  'font/otf',
134*d43fb2a3SEvan Bacon  'font/woff',
135*d43fb2a3SEvan Bacon  'font/woff2',
136*d43fb2a3SEvan Bacon  // Images
137*d43fb2a3SEvan Bacon  'image/avif',
138*d43fb2a3SEvan Bacon  'image/bmp',
139*d43fb2a3SEvan Bacon  'image/gif',
140*d43fb2a3SEvan Bacon  'image/jpeg',
141*d43fb2a3SEvan Bacon  'image/png',
142*d43fb2a3SEvan Bacon  'image/tiff',
143*d43fb2a3SEvan Bacon  'image/vnd.microsoft.icon',
144*d43fb2a3SEvan Bacon  'image/webp',
145*d43fb2a3SEvan Bacon  // Audio
146*d43fb2a3SEvan Bacon  'audio/3gpp',
147*d43fb2a3SEvan Bacon  'audio/aac',
148*d43fb2a3SEvan Bacon  'audio/basic',
149*d43fb2a3SEvan Bacon  'audio/mpeg',
150*d43fb2a3SEvan Bacon  'audio/ogg',
151*d43fb2a3SEvan Bacon  'audio/wav',
152*d43fb2a3SEvan Bacon  'audio/webm',
153*d43fb2a3SEvan Bacon  'audio/x-aiff',
154*d43fb2a3SEvan Bacon  'audio/x-midi',
155*d43fb2a3SEvan Bacon  'audio/x-wav',
156*d43fb2a3SEvan Bacon  // Video
157*d43fb2a3SEvan Bacon  'video/3gpp',
158*d43fb2a3SEvan Bacon  'video/mp2t',
159*d43fb2a3SEvan Bacon  'video/mpeg',
160*d43fb2a3SEvan Bacon  'video/ogg',
161*d43fb2a3SEvan Bacon  'video/quicktime',
162*d43fb2a3SEvan Bacon  'video/webm',
163*d43fb2a3SEvan Bacon  'video/x-msvideo',
164*d43fb2a3SEvan Bacon  // Archives
165*d43fb2a3SEvan Bacon  'application/java-archive',
166*d43fb2a3SEvan Bacon  'application/vnd.apple.installer+xml',
167*d43fb2a3SEvan Bacon  'application/x-7z-compressed',
168*d43fb2a3SEvan Bacon  'application/x-apple-diskimage',
169*d43fb2a3SEvan Bacon  'application/x-bzip',
170*d43fb2a3SEvan Bacon  'application/x-bzip2',
171*d43fb2a3SEvan Bacon  'application/x-gzip',
172*d43fb2a3SEvan Bacon  'application/x-java-archive',
173*d43fb2a3SEvan Bacon  'application/x-rar-compressed',
174*d43fb2a3SEvan Bacon  'application/x-tar',
175*d43fb2a3SEvan Bacon  'application/x-zip',
176*d43fb2a3SEvan Bacon  'application/zip',
177*d43fb2a3SEvan Bacon];
178*d43fb2a3SEvan Bacon
179*d43fb2a3SEvan Baconexport function isBinaryType(contentType: string | null | undefined) {
180*d43fb2a3SEvan Bacon  if (!contentType) return false;
181*d43fb2a3SEvan Bacon  const [test] = contentType.split(';');
182*d43fb2a3SEvan Bacon  return binaryTypes.includes(test);
183*d43fb2a3SEvan Bacon}
184