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