summaryrefslogtreecommitdiff
path: root/packages/integrations/netlify/src/netlify-functions.ts
blob: 7ca5510221338395a3285c2d97a323c4b48358c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { polyfill } from '@astrojs/webapi';
import type { Handler } from '@netlify/functions';
import { SSRManifest } from 'astro';
import { App } from 'astro/app';

polyfill(globalThis, {
	exclude: 'window document',
});

export interface Args {
	binaryMediaTypes?: string[];
}

function parseContentType(header?: string) {
	return header?.split(';')[0] ?? '';
}

const clientAddressSymbol = Symbol.for('astro.clientAddress');

export const createExports = (manifest: SSRManifest, args: Args) => {
	const app = new App(manifest);

	const binaryMediaTypes = args.binaryMediaTypes ?? [];
	const knownBinaryMediaTypes = new Set([
		'audio/3gpp',
		'audio/3gpp2',
		'audio/aac',
		'audio/midi',
		'audio/mpeg',
		'audio/ogg',
		'audio/opus',
		'audio/wav',
		'audio/webm',
		'audio/x-midi',
		'image/avif',
		'image/bmp',
		'image/gif',
		'image/vnd.microsoft.icon',
		'image/heif',
		'image/jpeg',
		'image/png',
		'image/svg+xml',
		'image/tiff',
		'image/webp',
		'video/3gpp',
		'video/3gpp2',
		'video/mp2t',
		'video/mp4',
		'video/mpeg',
		'video/ogg',
		'video/x-msvideo',
		'video/webm',
		...binaryMediaTypes,
	]);

	const handler: Handler = async (event) => {
		const { httpMethod, headers, rawUrl, body: requestBody, isBase64Encoded } = event;
		const init: RequestInit = {
			method: httpMethod,
			headers: new Headers(headers as any),
		};
		// Attach the event body the the request, with proper encoding.
		if (httpMethod !== 'GET' && httpMethod !== 'HEAD') {
			const encoding = isBase64Encoded ? 'base64' : 'utf-8';
			init.body =
				typeof requestBody === 'string' ? Buffer.from(requestBody, encoding) : requestBody;
		}
		const request = new Request(rawUrl, init);

		let routeData = app.match(request, { matchNotFound: true });

		if (!routeData) {
			return {
				statusCode: 404,
				body: 'Not found',
			};
		}

		const ip = headers['x-nf-client-connection-ip'];
		Reflect.set(request, clientAddressSymbol, ip);

		const response: Response = await app.render(request, routeData);
		const responseHeaders = Object.fromEntries(response.headers.entries());

		const responseContentType = parseContentType(responseHeaders['content-type']);
		const responseIsBase64Encoded = knownBinaryMediaTypes.has(responseContentType);

		let responseBody: string;
		if (responseIsBase64Encoded) {
			const ab = await response.arrayBuffer();
			responseBody = Buffer.from(ab).toString('base64');
		} else {
			responseBody = await response.text();
		}

		const fnResponse: any = {
			statusCode: response.status,
			headers: responseHeaders,
			body: responseBody,
			isBase64Encoded: responseIsBase64Encoded,
		};

		// Special-case set-cookie which has to be set an different way :/
		// The fetch API does not have a way to get multiples of a single header, but instead concatenates
		// them. There are non-standard ways to do it, and node-fetch gives us headers.raw()
		// See https://github.com/whatwg/fetch/issues/973 for discussion
		if (response.headers.has('set-cookie') && 'raw' in response.headers) {
			// Node fetch allows you to get the raw headers, which includes multiples of the same type.
			// This is needed because Set-Cookie *must* be called for each cookie, and can't be
			// concatenated together.
			type HeadersWithRaw = Headers & {
				raw: () => Record<string, string[]>;
			};

			const rawPacked = (response.headers as HeadersWithRaw).raw();
			if ('set-cookie' in rawPacked) {
				fnResponse.multiValueHeaders = {
					'set-cookie': rawPacked['set-cookie'],
				};
			}
		}

		// Apply cookies set via Astro.cookies.set/delete
		if (app.setCookieHeaders) {
			const setCookieHeaders = Array.from(app.setCookieHeaders(response));
			fnResponse.multiValueHeaders = fnResponse.multiValueHeaders || {};
			if (!fnResponse.multiValueHeaders['set-cookie']) {
				fnResponse.multiValueHeaders['set-cookie'] = [];
			}
			fnResponse.multiValueHeaders['set-cookie'].push(...setCookieHeaders);
		}

		return fnResponse;
	};

	return { handler };
};