Skip to content

Commit eba7174

Browse files
committed
Added maintenance banner when fractal-server is down
1 parent 821a4ad commit eba7174

File tree

7 files changed

+131
-66
lines changed

7 files changed

+131
-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: 96 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,82 @@
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('/_app')) {
21+
// The _app folder contains static files that are the result of npm build.
22+
// The hooks handle function is not called when loading valid static files, however we can
23+
// reach this point if a not existing file is requested. That could happen after an update
24+
// if the browser is loading a cached page that references to outdated contents.
25+
// In that case we can just skip the whole function.
26+
await resolve(event);
27+
}
28+
29+
if (event.url.pathname.startsWith('/api')) {
30+
// API page - AJAX request - handled in proxy'
31+
logger.trace('API endpoint detected, leaving the handling to the proxy');
32+
return await resolve(event);
33+
}
34+
835
logger.info('[%s] - %s', event.request.method, event.url.pathname);
936

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

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

2471
// Authentication guard
25-
const fastApiUsersAuth = event.cookies.get('fastapiusersauth');
26-
if (!fastApiUsersAuth) {
72+
if (!isPublicPage && userInfo === null) {
2773
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-
});
74+
throw redirect(302, '/auth/login?invalidate=true');
4175
}
4276

77+
// Admin area check
4378
if (event.url.pathname.startsWith('/v1/admin') || event.url.pathname.startsWith('/v2/admin')) {
44-
const user = await currentUser.json();
45-
if (!user.is_superuser) {
79+
if (!(/** @type {import('$lib/types').User} */ (userInfo).is_superuser)) {
4680
throw error(403, `Only superusers can access the admin area`);
4781
}
4882
}
@@ -57,11 +91,6 @@ export async function handleFetch({ event, request, fetch }) {
5791
1. https://github.com/fractal-analytics-platform/fractal-web/issues/274
5892
2. https://kit.svelte.dev/docs/hooks#server-hooks-handlefetch
5993
*/
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-
6594
if (request.url.startsWith(env.FRACTAL_SERVER_HOST)) {
6695
logger.trace('Including cookie into request to %s, via handleFetch', request.url);
6796
const cookie = event.request.headers.get('cookie');
@@ -71,3 +100,41 @@ export async function handleFetch({ event, request, fetch }) {
71100
}
72101
return fetch(request);
73102
}
103+
104+
/**
105+
* @param {typeof fetch} fetch
106+
* @returns {Promise<{ alive: boolean, version: string | null }>}
107+
*/
108+
async function getServerInfo(fetch) {
109+
let serverInfo = { alive: false, version: null };
110+
111+
try {
112+
const serverInfoResponse = await fetch(env.FRACTAL_SERVER_HOST + '/api/alive/');
113+
if (serverInfoResponse.ok) {
114+
serverInfo = await serverInfoResponse.json();
115+
logger.debug('Server info loaded: Alive %s - %s', serverInfo.alive, serverInfo.version);
116+
} else {
117+
logger.error(
118+
'Alive endpoint replied with unsuccessful status code %d',
119+
serverInfoResponse.status
120+
);
121+
}
122+
} catch (error) {
123+
logger.fatal('Error loading server info', error);
124+
}
125+
126+
return serverInfo;
127+
}
128+
129+
/**
130+
* @param {typeof fetch} fetch
131+
* @returns {Promise<import('$lib/types').User|null>}
132+
*/
133+
async function getUserInfo(fetch) {
134+
try {
135+
return await getCurrentUser(fetch);
136+
} catch (error) {
137+
logger.error('Error loading user info', error);
138+
return null;
139+
}
140+
}

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',

0 commit comments

Comments
 (0)