diff --git a/app/client/models/AdminChecks.ts b/app/client/models/AdminChecks.ts index da720306..1ad4c87b 100644 --- a/app/client/models/AdminChecks.ts +++ b/app/client/models/AdminChecks.ts @@ -169,6 +169,16 @@ It is good practice not to run Grist as the root user. 'reachable': { info: ` The main page of Grist should be available. +` + }, + + 'websockets': { + 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. See this +documentation for more explanation. ` }, }; diff --git a/app/common/BootProbe.ts b/app/common/BootProbe.ts index 753af1c4..699d3515 100644 --- a/app/common/BootProbe.ts +++ b/app/common/BootProbe.ts @@ -6,7 +6,8 @@ export type BootProbeIds = 'reachable' | 'host-header' | 'sandboxing' | - 'system-user' + 'system-user' | + 'websockets' ; export interface BootProbeResult { diff --git a/app/server/lib/BootProbes.ts b/app/server/lib/BootProbes.ts index 242ab0d8..3d6783f3 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'; /** @@ -58,6 +59,7 @@ export class BootProbes { this._probes.push(_bootProbe); this._probes.push(_hostHeaderProbe); this._probes.push(_sandboxingProbe); + this._probes.push(_webSocketsProbe); this._probeById = new Map(this._probes.map(p => [p.id, p])); } } @@ -104,6 +106,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',