aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/netlify/src/ssr-function.ts
blob: dba0dbead7bd9ab1b490705c01ce3128b7da898a (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
import type { Context } from '@netlify/functions';
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
import { applyPolyfills } from 'astro/app/node';

// Won't throw if the virtual module is not available because it's not supported in
// the users's astro version or if astro:env is not enabled in the project
await import('astro/env/setup')
	.then((mod) => mod.setGetEnv((key) => process.env[key]))
	.catch(() => {});

applyPolyfills();

export interface Args {
	middlewareSecret: string;
}

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

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

	function createHandler(integrationConfig: {
		cacheOnDemandPages: boolean;
		notFoundContent?: string;
	}) {
		return async function handler(request: Request, context: Context) {
			const routeData = app.match(request);
			if (!routeData && typeof integrationConfig.notFoundContent !== 'undefined') {
				return new Response(integrationConfig.notFoundContent, {
					status: 404,
					headers: { 'Content-Type': 'text/html; charset=utf-8' },
				});
			}

			Reflect.set(request, clientAddressSymbol, context.ip);
			let locals: Record<string, unknown> = {};

			const astroLocalsHeader = request.headers.get('x-astro-locals');
			const middlewareSecretHeader = request.headers.get('x-astro-middleware-secret');
			if (astroLocalsHeader) {
				if (middlewareSecretHeader !== middlewareSecret) {
					return new Response('Forbidden', { status: 403 });
				}
				// hide the secret from the rest of user and library code
				request.headers.delete('x-astro-middleware-secret');
				locals = JSON.parse(astroLocalsHeader);
			}

			locals.netlify = { context };

			const response = await app.render(request, { routeData, locals });

			if (app.setCookieHeaders) {
				for (const setCookieHeader of app.setCookieHeaders(response)) {
					response.headers.append('Set-Cookie', setCookieHeader);
				}
			}

			if (integrationConfig.cacheOnDemandPages) {
				const isCacheableMethod = ['GET', 'HEAD'].includes(request.method);

				// any user-provided Cache-Control headers take precedence
				const hasCacheControl = [
					'Cache-Control',
					'CDN-Cache-Control',
					'Netlify-CDN-Cache-Control',
				].some((header) => response.headers.has(header));

				if (isCacheableMethod && !hasCacheControl) {
					// caches this page for up to a year
					response.headers.append('CDN-Cache-Control', 'public, max-age=31536000, must-revalidate');
				}
			}

			return response;
		};
	}

	return { default: createHandler };
};