File size: 6,098 Bytes
bc20498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import * as set_cookie_parser from 'set-cookie-parser';
import { respond } from './respond.js';
import * as paths from '__sveltekit/paths';

/**
 * @param {{
 *   event: import('@sveltejs/kit').RequestEvent;
 *   options: import('types').SSROptions;
 *   manifest: import('@sveltejs/kit').SSRManifest;
 *   state: import('types').SSRState;
 *   get_cookie_header: (url: URL, header: string | null) => string;
 *   set_internal: (name: string, value: string, opts: import('./page/types.js').Cookie['options']) => void;
 * }} opts
 * @returns {typeof fetch}
 */
export function create_fetch({ event, options, manifest, state, get_cookie_header, set_internal }) {
	/**
	 * @type {typeof fetch}
	 */
	const server_fetch = async (info, init) => {
		const original_request = normalize_fetch_input(info, init, event.url);

		// some runtimes (e.g. Cloudflare) error if you access `request.mode`,
		// annoyingly, so we need to read the value from the `init` object instead
		let mode = (info instanceof Request ? info.mode : init?.mode) ?? 'cors';
		let credentials =
			(info instanceof Request ? info.credentials : init?.credentials) ?? 'same-origin';

		return options.hooks.handleFetch({
			event,
			request: original_request,
			fetch: async (info, init) => {
				const request = normalize_fetch_input(info, init, event.url);

				const url = new URL(request.url);

				if (!request.headers.has('origin')) {
					request.headers.set('origin', event.url.origin);
				}

				if (info !== original_request) {
					mode = (info instanceof Request ? info.mode : init?.mode) ?? 'cors';
					credentials =
						(info instanceof Request ? info.credentials : init?.credentials) ?? 'same-origin';
				}

				// Remove Origin, according to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin#description
				if (
					(request.method === 'GET' || request.method === 'HEAD') &&
					((mode === 'no-cors' && url.origin !== event.url.origin) ||
						url.origin === event.url.origin)
				) {
					request.headers.delete('origin');
				}

				if (url.origin !== event.url.origin) {
					// Allow cookie passthrough for "credentials: same-origin" and "credentials: include"
					// if SvelteKit is serving my.domain.com:
					// -        domain.com WILL NOT receive cookies
					// -     my.domain.com WILL receive cookies
					// -    api.domain.dom WILL NOT receive cookies
					// - sub.my.domain.com WILL receive cookies
					// ports do not affect the resolution
					// leading dot prevents mydomain.com matching domain.com
					// Do not forward other cookies for "credentials: include" because we don't know
					// which cookie belongs to which domain (browser does not pass this info)
					if (`.${url.hostname}`.endsWith(`.${event.url.hostname}`) && credentials !== 'omit') {
						const cookie = get_cookie_header(url, request.headers.get('cookie'));
						if (cookie) request.headers.set('cookie', cookie);
					}

					return fetch(request);
				}

				// handle fetch requests for static assets. e.g. prebaked data, etc.
				// we need to support everything the browser's fetch supports
				const prefix = paths.assets || paths.base;
				const decoded = decodeURIComponent(url.pathname);
				const filename = (
					decoded.startsWith(prefix) ? decoded.slice(prefix.length) : decoded
				).slice(1);
				const filename_html = `${filename}/index.html`; // path may also match path/index.html

				const is_asset = manifest.assets.has(filename);
				const is_asset_html = manifest.assets.has(filename_html);

				if (is_asset || is_asset_html) {
					const file = is_asset ? filename : filename_html;

					if (state.read) {
						const type = is_asset
							? manifest.mimeTypes[filename.slice(filename.lastIndexOf('.'))]
							: 'text/html';

						return new Response(state.read(file), {
							headers: type ? { 'content-type': type } : {}
						});
					}

					return await fetch(request);
				}

				if (credentials !== 'omit') {
					const cookie = get_cookie_header(url, request.headers.get('cookie'));
					if (cookie) {
						request.headers.set('cookie', cookie);
					}

					const authorization = event.request.headers.get('authorization');
					if (authorization && !request.headers.has('authorization')) {
						request.headers.set('authorization', authorization);
					}
				}

				if (!request.headers.has('accept')) {
					request.headers.set('accept', '*/*');
				}

				if (!request.headers.has('accept-language')) {
					request.headers.set(
						'accept-language',
						/** @type {string} */ (event.request.headers.get('accept-language'))
					);
				}

				const response = await respond(request, options, manifest, {
					...state,
					depth: state.depth + 1
				});

				const set_cookie = response.headers.get('set-cookie');
				if (set_cookie) {
					for (const str of set_cookie_parser.splitCookiesString(set_cookie)) {
						const { name, value, ...options } = set_cookie_parser.parseString(str, {
							decodeValues: false
						});

						const path = options.path ?? (url.pathname.split('/').slice(0, -1).join('/') || '/');

						// options.sameSite is string, something more specific is required - type cast is safe
						set_internal(name, value, {
							path,
							encode: (value) => value,
							.../** @type {import('cookie').CookieSerializeOptions} */ (options)
						});
					}
				}

				return response;
			}
		});
	};

	// Don't make this function `async`! Otherwise, the user has to `catch` promises they use for streaming responses or else
	// it will be an unhandled rejection. Instead, we add a `.catch(() => {})` ourselves below to prevent this from happening.
	return (input, init) => {
		// See docs in fetch.js for why we need to do this
		const response = server_fetch(input, init);
		response.catch(() => {});
		return response;
	};
}

/**
 * @param {RequestInfo | URL} info
 * @param {RequestInit | undefined} init
 * @param {URL} url
 */
function normalize_fetch_input(info, init, url) {
	if (info instanceof Request) {
		return info;
	}

	return new Request(typeof info === 'string' ? new URL(info, url) : info, init);
}