mirror of
https://github.com/Lissy93/dashy.git
synced 2026-07-30 11:06:09 -04:00
⚡️ Improve compatibility of status checks
This commit is contained in:
@@ -98,7 +98,7 @@ If the URL you are checking has an unsigned certificate, or is not using HTTPS,
|
||||
|
||||
If your service is online, but responds with a status code that is not in the 2xx range, then you can use `statusCheckAcceptCodes` to set an accepted status code.
|
||||
|
||||
If you get an error, like `Service Unavailable: Server resulted in a fatal error`, even when it's definitely online, this is most likely caused by missing the protocol. Don't forget to include `https://` (or whatever protocol) before the URL, and ensure that if needed, you've specified the port.
|
||||
If you get an error, like `Service Unavailable: Server resulted in Invalid URL`, even when it's definitely online, this is most likely caused by missing the protocol. Don't forget to include `https://` (or whatever protocol) before the URL, and ensure that if needed, you've specified the port.
|
||||
|
||||
Running Dashy in HOST network mode, instead of BRIDGE will allow status check access to other services in HOST mode. For more info, see [#445](https://github.com/Lissy93/dashy/discussions/445).
|
||||
|
||||
|
||||
@@ -728,7 +728,7 @@ If the URL you are checking has an unsigned certificate, or is not using HTTPS,
|
||||
|
||||
If your service is online, but responds with a status code that is not in the 2xx range, then you can use `statusCheckAcceptCodes` to set an accepted status code.
|
||||
|
||||
If you get an error, like `Service Unavailable: Server resulted in a fatal error`, even when it's definitely online, this is most likely caused by missing the protocol. Don't forget to include `https://` (or whatever protocol) before the URL, and ensure that if needed, you've specified the port.
|
||||
If you get an error, like `Service Unavailable: Server resulted in Invalid URL`, even when it's definitely online, this is most likely caused by missing the protocol. Don't forget to include `https://` (or whatever protocol) before the URL, and ensure that if needed, you've specified the port.
|
||||
|
||||
Running Dashy in HOST network mode, instead of BRIDGE will allow status check access to other services in HOST mode. For more info, see [#445](https://github.com/Lissy93/dashy/discussions/445).
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ module.exports = (paramStr, render) => {
|
||||
if (!paramStr || !paramStr.includes('=')) {
|
||||
immediateError(render);
|
||||
} else {
|
||||
// Prepare the parameters, which are got from the URL
|
||||
const params = new URLSearchParams(paramStr);
|
||||
const host = decodeURIComponent(params.get('host'));
|
||||
const count = Number(decodeURIComponent(params.get('count'))) || 2;
|
||||
const timeout = Number(decodeURIComponent(params.get('timeout'))) || 2000;
|
||||
// Get the url to check from query params
|
||||
const params = new URLSearchParams(paramStr.slice(paramStr.indexOf('?') + 1));
|
||||
const host = params.get('host') || '';
|
||||
const count = Number(params.get('count')) || 2;
|
||||
const timeout = Number(params.get('timeout')) || 2000;
|
||||
if (!host || typeof host !== 'string') {
|
||||
immediateError(render, 'Invalid host given for ping check.');
|
||||
return;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* endpoint, and then resolve the response status code, time taken, and short message
|
||||
*/
|
||||
const request = require('../utils/request');
|
||||
|
||||
const { validateTargetUrl } = request;
|
||||
|
||||
/* Determines if successful from the HTTP response code */
|
||||
@@ -22,7 +23,7 @@ const makeMessageText = (data) => `${data.successStatus ? '✅' : '⚠️'} `
|
||||
|
||||
/* Makes human-readable response text for failed check */
|
||||
const makeErrorMessage = (data) => `❌ Service Unavailable: ${data.hostname || 'Server'} `
|
||||
+ `resulted in ${data.code || 'a fatal error'} ${data.errno ? `(${data.errno})` : ''}`;
|
||||
+ `resulted in ${data.code || data.message || 'a fatal error'} ${data.errno ? `(${data.errno})` : ''}`;
|
||||
|
||||
const makeErrorMessage2 = (data) => '❌ Service Error - '
|
||||
+ `${data.status} - ${data.statusText}`;
|
||||
@@ -81,11 +82,10 @@ const makeRequest = (url, options, render) => {
|
||||
|
||||
const decodeHeaders = (maybeHeaders) => {
|
||||
if (!maybeHeaders) return {};
|
||||
const decodedHeaders = decodeURIComponent(maybeHeaders);
|
||||
let parsedHeaders = {};
|
||||
try {
|
||||
parsedHeaders = JSON.parse(decodedHeaders);
|
||||
} catch (e) { /* Not valid JSON, will just return false */ }
|
||||
parsedHeaders = JSON.parse(maybeHeaders);
|
||||
} catch (e) { /* Not valid JSON, will just return an empty object */ }
|
||||
return parsedHeaders;
|
||||
};
|
||||
|
||||
@@ -102,11 +102,11 @@ module.exports = (paramStr, render) => {
|
||||
if (!paramStr || !paramStr.includes('=')) {
|
||||
immediateError(render);
|
||||
} else {
|
||||
// Prepare the parameters, which are got from the URL
|
||||
const params = new URLSearchParams(paramStr);
|
||||
const url = decodeURIComponent(params.get('url'));
|
||||
const acceptCodes = decodeURIComponent(params.get('acceptCodes'));
|
||||
const maxRedirects = decodeURIComponent(params.get('maxRedirects')) || 0;
|
||||
// Read the query params from req.url, ignoring its leading path and '?'
|
||||
const params = new URLSearchParams(paramStr.slice(paramStr.indexOf('?') + 1));
|
||||
const url = params.get('url') || '';
|
||||
const acceptCodes = params.get('acceptCodes');
|
||||
const maxRedirects = Number(params.get('maxRedirects')) || 5;
|
||||
const headers = decodeHeaders(params.get('headers'));
|
||||
const enableInsecure = !!params.get('enableInsecure');
|
||||
if (!url || url === 'undefined') {
|
||||
|
||||
@@ -50,4 +50,7 @@ const runCheck = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
runCheck().catch(() => { /* never propagate — module-load must not throw */ });
|
||||
// Skip version check when running tests
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
runCheck().catch(() => { /* just chillin */ });
|
||||
}
|
||||
|
||||
@@ -58,9 +58,9 @@ export default {
|
||||
},
|
||||
/* Determine how often to re-fire status checks */
|
||||
statusCheckInterval() {
|
||||
let interval = this.item.statusCheckInterval || this.appConfig.statusCheckInterval;
|
||||
let interval = this.item.statusCheckInterval ?? this.appConfig.statusCheckInterval;
|
||||
if (!interval) return 0;
|
||||
if (interval > 60) interval = 60;
|
||||
if (interval > 300) interval = 300;
|
||||
if (interval < 1) interval = 0;
|
||||
return interval;
|
||||
},
|
||||
@@ -79,8 +79,7 @@ export default {
|
||||
},
|
||||
/* Determine how often to re-fire ping checks */
|
||||
pingCheckInterval() {
|
||||
let interval = this.item.pingCheckInterval;
|
||||
if (!interval) interval = this.appConfig.pingCheckInterval;
|
||||
let interval = this.item.pingCheckInterval ?? this.appConfig.pingCheckInterval;
|
||||
if (!interval) return 0;
|
||||
interval = Math.floor(interval);
|
||||
if (interval < 1) return 0;
|
||||
@@ -211,8 +210,8 @@ export default {
|
||||
})
|
||||
.catch(() => { // Something went very wrong.
|
||||
this.statusResponse = {
|
||||
statusText: 'Failed to make request',
|
||||
statusSuccess: false,
|
||||
successStatus: false,
|
||||
message: 'Failed to make request',
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -221,8 +220,8 @@ export default {
|
||||
if (!this.isPingCheckEnabled) return;
|
||||
if (!this.pingCheckHost) {
|
||||
this.pingResponse = {
|
||||
statusText: 'Host not set or invalid',
|
||||
statusSuccess: false,
|
||||
successStatus: false,
|
||||
message: 'Host not set or invalid',
|
||||
};
|
||||
} else {
|
||||
if (this.pingResponse) this.pingResponse.successStatus = undefined; // Reset previous response, to show loading state
|
||||
@@ -233,8 +232,8 @@ export default {
|
||||
})
|
||||
.catch(() => { // Something went very wrong.
|
||||
this.pingResponse = {
|
||||
statusText: 'Failed to make Ping request',
|
||||
statusSuccess: false,
|
||||
successStatus: false,
|
||||
message: 'Failed to make Ping request',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1336,6 +1336,12 @@
|
||||
"default": 0,
|
||||
"description": "If your service redirects to another page, and you would like status checks to follow redirects, then specify the maximum number of redirects here"
|
||||
},
|
||||
"statusCheckInterval": {
|
||||
"title": "Item Status Check Interval",
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "How often (in seconds) to recheck this item's status. If set to 0, status is only checked on page load. Limited to a 5 minute maximum. Will override appConfig.statusCheckInterval."
|
||||
},
|
||||
"pingCheckEnabled": {
|
||||
"title": "Item Ping Check Enabling",
|
||||
"type": "boolean",
|
||||
|
||||
@@ -261,13 +261,13 @@ describe('Computed: statusCheckInterval', () => {
|
||||
expect(wrapper.vm.statusCheckInterval).toBe(15);
|
||||
});
|
||||
|
||||
it('clamps to max 60', () => {
|
||||
it('clamps to a 5 minute maximum', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', statusCheckInterval: 120,
|
||||
id: '1', title: 'X', url: '#', statusCheckInterval: 600,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.statusCheckInterval).toBe(60);
|
||||
expect(wrapper.vm.statusCheckInterval).toBe(300);
|
||||
});
|
||||
|
||||
it('clamps values less than 1 to 0', () => {
|
||||
|
||||
@@ -2,48 +2,98 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import http from 'http';
|
||||
import {
|
||||
describe, it, expect, beforeAll, afterAll,
|
||||
} from 'vitest';
|
||||
import request from 'supertest';
|
||||
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashy-status-test-'));
|
||||
fs.writeFileSync(path.join(tmpDir, 'conf.yml'), 'pageInfo:\n title: Test\nsections: []\n');
|
||||
process.env.USER_DATA_DIR = tmpDir;
|
||||
|
||||
const app = require('../../services/app');
|
||||
|
||||
let server;
|
||||
let base;
|
||||
|
||||
// A small local server that plays out each status-check scenario
|
||||
beforeAll(async () => {
|
||||
server = http.createServer((req, res) => {
|
||||
switch (req.url.split('?')[0]) {
|
||||
case '/ok': res.writeHead(200); break;
|
||||
case '/down': res.writeHead(503); break;
|
||||
case '/teapot': res.writeHead(418); break;
|
||||
case '/redirect': res.writeHead(302, { Location: '/ok' }); break;
|
||||
case '/redirect-loop': res.writeHead(302, { Location: '/redirect-loop' }); break;
|
||||
case '/needs-header': res.writeHead(req.headers['x-key'] ? 200 : 401); break;
|
||||
default: res.writeHead(404);
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
await new Promise((resolve) => { server.listen(0, '127.0.0.1', resolve); });
|
||||
base = `http://127.0.0.1:${server.address().port}`;
|
||||
});
|
||||
|
||||
afterAll(() => new Promise((resolve) => { server.close(resolve); }));
|
||||
|
||||
// Run a status check against a url and return the parsed result
|
||||
const check = async (url, extra = '') => {
|
||||
const res = await request(app).get(`/status-check/?&url=${encodeURIComponent(url)}${extra}`);
|
||||
return JSON.parse(res.text);
|
||||
};
|
||||
|
||||
describe('Status check', () => {
|
||||
it('returns error for missing URL param', async () => {
|
||||
const res = await request(app).get('/status-check/');
|
||||
const body = JSON.parse(res.text);
|
||||
expect(body.successStatus).toBe(false);
|
||||
it('shows a service that returns 200 as online', async () => {
|
||||
expect((await check(`${base}/ok`)).successStatus).toBe(true);
|
||||
});
|
||||
|
||||
it('returns error for empty URL param', async () => {
|
||||
const res = await request(app).get('/status-check/?&url=');
|
||||
const body = JSON.parse(res.text);
|
||||
expect(body.successStatus).toBe(false);
|
||||
it('shows a service that returns an error code as offline', async () => {
|
||||
expect((await check(`${base}/down`)).successStatus).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores POST requests', async () => {
|
||||
const res = await request(app).post('/status-check/?&url=x');
|
||||
expect(res.status).toBeLessThan(500);
|
||||
it('treats a status code you have allow-listed as a success', async () => {
|
||||
expect((await check(`${base}/teapot`, '&acceptCodes=418')).successStatus).toBe(true);
|
||||
});
|
||||
|
||||
it('follows redirects when they are allowed', async () => {
|
||||
expect((await check(`${base}/redirect`, '&maxRedirects=3')).successStatus).toBe(true);
|
||||
});
|
||||
|
||||
it('stops following once the redirect limit is hit', async () => {
|
||||
expect((await check(`${base}/redirect-loop`, '&maxRedirects=1')).successStatus).toBe(false);
|
||||
});
|
||||
|
||||
it('sends custom headers along with the request', async () => {
|
||||
// the % in the value would throw if the headers param were decoded twice
|
||||
const headers = encodeURIComponent(JSON.stringify({ 'X-Key': '50% off' }));
|
||||
expect((await check(`${base}/needs-header`, `&headers=${headers}`)).successStatus).toBe(true);
|
||||
});
|
||||
|
||||
it('reports an unreachable host as offline', async () => {
|
||||
expect((await check('http://127.0.0.1:1')).successStatus).toBe(false);
|
||||
});
|
||||
|
||||
it('returns a clear error when no url is given', async () => {
|
||||
const body = await check('');
|
||||
expect(body.successStatus).toBe(false);
|
||||
expect(body.message).toMatch(/Missing or Malformed/);
|
||||
});
|
||||
|
||||
it('reads the url even without the leading ampersand', async () => {
|
||||
const res = await request(app).get(`/status-check/?url=${encodeURIComponent(`${base}/ok`)}`);
|
||||
expect(JSON.parse(res.text).successStatus).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ping check', () => {
|
||||
it('returns error for missing URL param', async () => {
|
||||
it('returns an error when no host is given', async () => {
|
||||
const res = await request(app).get('/ping-check/?count=2');
|
||||
expect(JSON.parse(res.text).successStatus).toBe(false);
|
||||
});
|
||||
|
||||
it('returns an error for a request with no query', async () => {
|
||||
const res = await request(app).get('/ping-check/');
|
||||
const body = JSON.parse(res.text);
|
||||
expect(body.successStatus).toBe(false);
|
||||
});
|
||||
|
||||
it('returns error for empty URL param', async () => {
|
||||
const res = await request(app).get('/ping-check/?&host=');
|
||||
const body = JSON.parse(res.text);
|
||||
expect(body.successStatus).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores POST requests', async () => {
|
||||
const res = await request(app).post('/ping-check/?&host=localhost');
|
||||
expect(res.status).toBeLessThan(500);
|
||||
expect(JSON.parse(res.text).successStatus).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user