diff --git a/app/client/models/AdminChecks.ts b/app/client/models/AdminChecks.ts index 51ff8b2bd7..bd5a8cee49 100644 --- a/app/client/models/AdminChecks.ts +++ b/app/client/models/AdminChecks.ts @@ -163,6 +163,15 @@ It is good practice not to run Grist as the root user. 'reachable': { info: ` The main page of Grist should be available. +` + }, + + 'websockets': { + // TODO: add a link to https://support.getgrist.com/self-managed/#how-do-i-run-grist-on-a-server + info: ` +Websocket connections need HTTP 1.1 and the ability to pass a few +extra headers in order to work. Sometimes a reverse proxy can +interfere with these requirements. ` }, }; diff --git a/app/common/BootProbe.ts b/app/common/BootProbe.ts index dd86704937..0da3c859fc 100644 --- a/app/common/BootProbe.ts +++ b/app/common/BootProbe.ts @@ -7,7 +7,8 @@ export type BootProbeIds = 'host-header' | 'sandboxing' | 'system-user' | - 'authentication' + 'authentication' | + 'websockets' ; export interface BootProbeResult { diff --git a/app/server/lib/BootProbes.ts b/app/server/lib/BootProbes.ts index b1186c51a4..8145f2a2ce 100644 --- a/app/server/lib/BootProbes.ts +++ b/app/server/lib/BootProbes.ts @@ -4,6 +4,7 @@ import { removeTrailingSlash } from 'app/common/gutil'; import { expressWrap, jsonErrorHandler } from 'app/server/lib/expressWrap'; import { GristServer } from 'app/server/lib/GristServer'; import * as express from 'express'; +import WS from 'ws'; import fetch from 'node-fetch'; /** @@ -59,6 +60,7 @@ export class BootProbes { this._probes.push(_hostHeaderProbe); this._probes.push(_sandboxingProbe); this._probes.push(_authenticationProbe); + this._probes.push(_webSocketsProbe); this._probeById = new Map(this._probes.map(p => [p.id, p])); } } @@ -105,6 +107,37 @@ const _homeUrlReachableProbe: Probe = { } }; +const _webSocketsProbe: Probe = { + id: 'websockets', + name: 'Can we open a websocket with the server', + apply: async (server, req) => { + return new Promise((resolve) => { + const url = new URL(server.getHomeUrl(req)); + url.protocol = (url.protocol === 'https:') ? 'wss:' : 'ws:'; + const ws = new WS.WebSocket(url.href); + const details: Record = { + url, + }; + ws.on('open', () => { + ws.send('Just nod if you can hear me.'); + resolve({ + success: true, + details, + }); + ws.close(); + }); + ws.on('error', (ev) => { + details.error = ev.message; + resolve({ + success: false, + details, + }); + ws.close(); + }); + }); + } +}; + const _statusCheckProbe: Probe = { id: 'health-check', name: 'Is an internal health check passing',