Skip to content

Commit 7278aae

Browse files
authored
Merge pull request #504 from fractal-analytics-platform/maintenance
Added maintenance banner when fractal-server is down
2 parents 821a4ad + d46830c commit 7278aae

File tree

8 files changed

+144
-66
lines changed

8 files changed

+144
-66
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ node_modules
77
.env.*
88
!.env.example
99
*.d.ts
10+
/venv
1011

1112
# Ignore files for PNPM, NPM and YARN
1213
pnpm-lock.yaml

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
*Note: Numbers like (\#123) point to closed Pull Requests on the fractal-web repository.*
22

3+
# Unreleased
4+
5+
* Added maintenance banner when fractal-server is down (\#504).
6+
37
# 1.1.0
48

59
> WARNING: with this release all the environment variables will be read from

src/hooks.server.js

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,87 @@
11
import { env } from '$env/dynamic/private';
2-
import { error } from '@sveltejs/kit';
2+
import { getCurrentUser } from '$lib/server/api/v1/auth_api';
33
import { getLogger } from '$lib/server/logger.js';
4+
import { error, redirect } from '@sveltejs/kit';
45

56
const logger = getLogger('hooks');
67

8+
if (env.FRACTAL_SERVER_HOST.endsWith('/')) {
9+
env.FRACTAL_SERVER_HOST = env.FRACTAL_SERVER_HOST.substring(
10+
0,
11+
env.FRACTAL_SERVER_HOST.length - 1
12+
);
13+
logger.trace(
14+
'Removing final slash from FRACTAL_SERVER_HOST, new value is %s',
15+
env.FRACTAL_SERVER_HOST
16+
);
17+
}
18+
719
export async function handle({ event, resolve }) {
20+
if (event.url.pathname.startsWith('/api')) {
21+
// API page - AJAX request - handled in proxy'
22+
logger.trace('API endpoint detected, leaving the handling to the proxy');
23+
return await resolve(event);
24+
}
25+
26+
if (event.route.id === null) {
27+
if (event.url.pathname.startsWith('/_app')) {
28+
// The _app folder contains static files that are the result of npm build.
29+
// The hooks handle function is not called when loading valid static files, however we can
30+
// reach this point if a not existing file is requested. That could happen after an update
31+
// if the browser is loading a cached page that references to outdated contents.
32+
// In that case we can usually ignore the logs.
33+
logger.trace('[%s] - %s - [NOT FOUND]', event.request.method, event.url.pathname);
34+
} else {
35+
logger.info('[%s] - %s - [NOT FOUND]', event.request.method, event.url.pathname);
36+
}
37+
throw error(404, 'Route not found');
38+
}
39+
840
logger.info('[%s] - %s', event.request.method, event.url.pathname);
941

10-
if (
42+
// Retrieve server info (alive and version)
43+
const serverInfo = await getServerInfo(event.fetch);
44+
45+
// Check if auth cookie is present
46+
const fastApiUsersAuth = event.cookies.get('fastapiusersauth');
47+
if (!fastApiUsersAuth) {
48+
logger.debug('No auth cookie found');
49+
}
50+
51+
// Retrieve user info
52+
let userInfo = null;
53+
if (serverInfo.alive && fastApiUsersAuth) {
54+
userInfo = await getUserInfo(event.fetch);
55+
logger.trace('User: %s', userInfo?.email);
56+
}
57+
58+
// Store the pageInfo in locals variable to use the in +layout.server.js
59+
event.locals['pageInfo'] = { serverInfo, userInfo };
60+
61+
const isPublicPage =
1162
event.url.pathname == '/' ||
1263
event.url.pathname.startsWith('/auth') ||
13-
event.url.pathname.startsWith('/sandbox/jsonschema')
14-
) {
64+
event.url.pathname.startsWith('/sandbox/jsonschema');
65+
66+
if (isPublicPage) {
1567
logger.debug('Public page - No auth required');
1668
return await resolve(event);
1769
}
1870

19-
if (event.url.pathname.startsWith('/api')) {
20-
// API page - AJAX request - handled in proxy'
21-
return await resolve(event);
71+
if (!serverInfo.alive && !isPublicPage) {
72+
// If fractal-server is not available, redirect to the home page to display the maintenance banner
73+
throw redirect(302, '/?invalidate=true');
2274
}
2375

2476
// Authentication guard
25-
const fastApiUsersAuth = event.cookies.get('fastapiusersauth');
26-
if (!fastApiUsersAuth) {
77+
if (!isPublicPage && userInfo === null) {
2778
logger.debug('Authentication required - No auth cookie found - Redirecting to login');
28-
return new Response(null, {
29-
status: 302,
30-
headers: { location: '/auth/login?invalidate=true' }
31-
});
32-
}
33-
34-
const currentUser = await event.fetch(`${env.FRACTAL_SERVER_HOST}/auth/current-user/`);
35-
if (!currentUser.ok) {
36-
logger.debug('Validation of authentication - Error loading user info');
37-
return new Response(null, {
38-
status: 302,
39-
headers: { location: '/auth/login?invalidate=true' }
40-
});
79+
throw redirect(302, '/auth/login?invalidate=true');
4180
}
4281

82+
// Admin area check
4383
if (event.url.pathname.startsWith('/v1/admin') || event.url.pathname.startsWith('/v2/admin')) {
44-
const user = await currentUser.json();
45-
if (!user.is_superuser) {
84+
if (!(/** @type {import('$lib/types').User} */ (userInfo).is_superuser)) {
4685
throw error(403, `Only superusers can access the admin area`);
4786
}
4887
}
@@ -57,11 +96,6 @@ export async function handleFetch({ event, request, fetch }) {
5796
1. https://github.com/fractal-analytics-platform/fractal-web/issues/274
5897
2. https://kit.svelte.dev/docs/hooks#server-hooks-handlefetch
5998
*/
60-
61-
if (env.FRACTAL_SERVER_HOST.endsWith('/')) {
62-
env.FRACTAL_SERVER_HOST = env.FRACTAL_SERVER_HOST.substring(0, env.FRACTAL_SERVER_HOST.length - 1);
63-
}
64-
6599
if (request.url.startsWith(env.FRACTAL_SERVER_HOST)) {
66100
logger.trace('Including cookie into request to %s, via handleFetch', request.url);
67101
const cookie = event.request.headers.get('cookie');
@@ -71,3 +105,41 @@ export async function handleFetch({ event, request, fetch }) {
71105
}
72106
return fetch(request);
73107
}
108+
109+
/**
110+
* @param {typeof fetch} fetch
111+
* @returns {Promise<{ alive: boolean, version: string | null }>}
112+
*/
113+
async function getServerInfo(fetch) {
114+
let serverInfo = { alive: false, version: null };
115+
116+
try {
117+
const serverInfoResponse = await fetch(env.FRACTAL_SERVER_HOST + '/api/alive/');
118+
if (serverInfoResponse.ok) {
119+
serverInfo = await serverInfoResponse.json();
120+
logger.debug('Server info loaded: Alive %s - %s', serverInfo.alive, serverInfo.version);
121+
} else {
122+
logger.error(
123+
'Alive endpoint replied with unsuccessful status code %d',
124+
serverInfoResponse.status
125+
);
126+
}
127+
} catch (error) {
128+
logger.fatal('Error loading server info', error);
129+
}
130+
131+
return serverInfo;
132+
}
133+
134+
/**
135+
* @param {typeof fetch} fetch
136+
* @returns {Promise<import('$lib/types').User|null>}
137+
*/
138+
async function getUserInfo(fetch) {
139+
try {
140+
return await getCurrentUser(fetch);
141+
} catch (error) {
142+
logger.error('Error loading user info', error);
143+
return null;
144+
}
145+
}

src/lib/server/api/v1/auth_api.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export async function userAuthentication(fetch, data) {
2929
/**
3030
* Fetches user identity
3131
* @param {typeof fetch} fetch
32-
* @returns {Promise<import('$lib/types').User>}
32+
* @returns {Promise<import('$lib/types').User|null>}
3333
*/
3434
export async function getCurrentUser(fetch) {
3535
logger.debug('Retrieving current user');
@@ -40,7 +40,7 @@ export async function getCurrentUser(fetch) {
4040

4141
if (!response.ok) {
4242
logger.warn('Unable to retrieve the current user');
43-
await responseError(response);
43+
return null;
4444
}
4545

4646
return await response.json();

src/routes/+layout.server.js

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,19 @@
1-
import { env } from '$env/dynamic/private';
2-
import { getCurrentUser } from '$lib/server/api/v1/auth_api';
31
import { getLogger } from '$lib/server/logger.js';
42

53
const logger = getLogger('page layout');
64

7-
export async function load({ fetch, cookies }) {
5+
export async function load({ locals, request, url }) {
86
// This is a mark to notify and log when the server is running SSR
97
logger.debug('SSR - Main layout');
108

11-
const serverInfo = await fetch(env.FRACTAL_SERVER_HOST + '/api/alive/')
12-
.then(async (res) => {
13-
const info = await res.json();
14-
logger.debug('Server info loaded: Alive %s - %s', info.alive, info.version);
15-
return info;
16-
})
17-
.catch((error) => {
18-
logger.fatal('Error loading server info', error);
19-
});
9+
logger.trace('[%s] - %s', request.method, url.pathname);
2010

21-
// Check user info
22-
// Check auth cookie is present
23-
const fastApiUsersAuth = cookies.get('fastapiusersauth');
24-
if (!fastApiUsersAuth) {
25-
logger.debug('No auth cookie found');
26-
return {
27-
serverInfo,
28-
userInfo: null
29-
};
30-
}
11+
// read pageInfo set from hooks.server.js
12+
const pageInfo = locals['pageInfo'];
3113

32-
const userInfo = await getCurrentUser(fetch).catch((error) => {
33-
logger.error('Error loading user info', error);
34-
return null;
35-
});
14+
if (!pageInfo) {
15+
logger.error('pageInfo is missing, it should have been loaded by hooks.server.js');
16+
}
3617

37-
return {
38-
serverInfo,
39-
userInfo
40-
};
18+
return pageInfo;
4119
}

src/routes/+layout.svelte

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script>
22
import { browser } from '$app/environment';
3+
import { afterNavigate, invalidateAll } from '$app/navigation';
34
import { page } from '$app/stores';
45
import { navigating } from '$app/stores';
56
import { reloadVersionedPage } from '$lib/common/selected_api_version';
@@ -102,6 +103,12 @@
102103
sessionStorage.setItem('userLoggedIn', 'true');
103104
}
104105
});
106+
107+
afterNavigate(async () => {
108+
if (location.href.includes('invalidate=true')) {
109+
await invalidateAll();
110+
}
111+
});
105112
</script>
106113
107114
<main>
@@ -208,6 +215,11 @@
208215
<div class="admin-border" />
209216
{/if}
210217
<div class="container p-4">
218+
{#if !server.alive}
219+
<div class="alert alert-danger">
220+
Sorry, we are performing some maintenance on fractal-server. It will be back online soon.
221+
</div>
222+
{/if}
211223
{#if userLoggedIn && !$page.data.userInfo.is_verified}
212224
<div class="row">
213225
<div class="col">
@@ -235,7 +247,10 @@
235247
<div class="col d-flex justify-content-center">
236248
<div class="hstack gap-3">
237249
<span class="font-monospace">
238-
fractal-server {server.version}, fractal-web {clientVersion}
250+
{#if server.version}
251+
fractal-server {server.version},
252+
{/if}
253+
fractal-web {clientVersion}
239254
</span>
240255
</div>
241256
</div>

src/routes/proxy.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const logger = getLogger('proxy');
99
export function createGetProxy(path) {
1010
return async function GET({ params, url, request }) {
1111
try {
12-
logger.trace('[GET] /%s/%s/%s', path, params.path, url.search);
12+
logger.info('[GET] - /%s/%s/%s', path, params.path, url.search);
1313
return await fetch(`${env.FRACTAL_SERVER_HOST}/${path}/${params.path}/${url.search}`, {
1414
method: 'GET',
1515
credentials: 'include',
@@ -28,7 +28,7 @@ export function createGetProxy(path) {
2828
export function createPostProxy(path) {
2929
return async function POST({ params, url, request }) {
3030
try {
31-
logger.trace('[POST] /%s/%s/%s', path, params.path, url.search);
31+
logger.info('[POST] - /%s/%s/%s', path, params.path, url.search);
3232
return await fetch(`${env.FRACTAL_SERVER_HOST}/${path}/${params.path}/${url.search}`, {
3333
method: 'POST',
3434
credentials: 'include',
@@ -48,7 +48,7 @@ export function createPostProxy(path) {
4848
export function createPatchProxy(path) {
4949
return async function PATCH({ params, url, request }) {
5050
try {
51-
logger.trace('[PATCH] /%s/%s/%s', path, params.path, url.search);
51+
logger.info('[PATCH] - /%s/%s/%s', path, params.path, url.search);
5252
return await fetch(`${env.FRACTAL_SERVER_HOST}/${path}/${params.path}/${url.search}`, {
5353
method: 'PATCH',
5454
credentials: 'include',
@@ -68,7 +68,7 @@ export function createPatchProxy(path) {
6868
export function createDeleteProxy(path) {
6969
return async function DELETE({ params, url, request }) {
7070
try {
71-
logger.trace('[DELETE] /%s/%s/%s', path, params.path, url.search);
71+
logger.info('[DELETE] - /%s/%s/%s', path, params.path, url.search);
7272
return await fetch(`${env.FRACTAL_SERVER_HOST}/${path}/${params.path}/${url.search}`, {
7373
method: 'DELETE',
7474
credentials: 'include',

tests/not_found.spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test('Handles not found pages', async ({ page }) => {
4+
await page.goto('/foo');
5+
await expect(page.getByText('Route not found')).toHaveCount(1);
6+
await page.goto('/_app/foo');
7+
await expect(page.getByText('Route not found')).toHaveCount(1);
8+
});

0 commit comments

Comments
 (0)