Chybova hlaseni konektoru rikaji, co se stalo, a kam to slo

Duvod od sluzby jde primo do hlasky: z tela odpovedi se vytahne detail,
error_description, message, title, error i seznam missingHeaders. Retezec,
ktery vypada jako JSON, se rozbaluje dal - sluzba iDoklad presne takhle
predava telo od iDokladu samotneho. Kdyz sluzba nenapsala nic, rekne se to.

401 a 403 uz nejsou jedna hlaska. 401 = udaje sluzba dostala a neuznala je.
403 = tvar udaju je v poradku, zakazuje se samo volani.

V kazde hlasce je cela adresa vcetne serveru (ScriptRequestInfo.url),
bez query - v query muze byt tajemstvi. Zaklad adresy je z konfigurace
a konektor ho smi prepsat, takze se neda odvodit z toho, kde je nasazeny
portal. Adresa je videt i na karte konektoru a v odpovedi na test, i kdyz
overeni projde.

Tlacitko Logy na karte konektoru a historie poslednich peti overeni.
Odpoved sluzby dosud existovala jen v odpovedi na test, tedy do prekresleni
stranky, a v logu containeru. Do logu containeru se nikdo divat nechodi.
Zaznam se uklada i pri uspechu, jinak by neslo poznat, jestli konektor nesel
nikdy, nebo prestal jit ve chvili, kdy nekdo sahnul na udaje.

Migrace 003_connector_checks.sql, endpoint GET /connectors/:id/checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-25 07:06:14 +02:00
co-authored by Claude Opus 5
parent 6297bbf480
commit 5a124a53d8
14 changed files with 648 additions and 54 deletions
+89 -18
View File
@@ -19,6 +19,7 @@ import { Router, type Request, type Response } from 'express';
import { z } from 'zod';
import { accessFor } from '../data/access.js';
import {
CHECK_HISTORY,
connectorCountsByService,
createConnector,
deleteConnector,
@@ -28,6 +29,7 @@ import {
toPublicConnector,
updateConnector,
validateConnectorValues,
type ConnectorCheck,
} from '../data/connectorStore.js';
import {
canSeeService,
@@ -255,6 +257,11 @@ connectorsRouter.delete('/:id', async (req, res) => {
* Vola `verifyPath` sluzby, coz je zamerne **cteci** volani, ktere vyzaduje
* autorizaci. Kdyz ho sluzba nema, overi se jen `/health` - a odpoved to rekne
* nahlas, aby si nikdo nemyslel, ze jsou overene i pristupove udaje.
*
* Ve vysledku je vzdy **cela adresa vcetne serveru**, i kdyz overeni projde.
* Zaklad adresy je z konfigurace (`SERVICES_BASE_URL`) a konektor ho smi
* prepsat, takze "vratilo 403" bez serveru nerika, jestli se to vubec trefilo
* na spravny stroj. Hadat to podle toho, kde je nasazeny portal, nejde.
*/
connectorsRouter.post('/:id/test', async (req, res) => {
const tenantId = tenantOrDeny(req, res);
@@ -269,16 +276,29 @@ connectorsRouter.post('/:id/test', async (req, res) => {
const target = resolveTarget(service.id, connector);
if (!target.ready) {
await setConnectorStatus(connector.id, 'error', target.missing.join(', '), [tenantId]);
return res.json({
ok: false,
checked: 'nic',
message: `Napojení není hotové: ${target.missing.join(', ')}.`,
});
const message = `Napojení není hotové: ${target.missing.join(', ')}.`;
await setConnectorStatus(
connector.id,
'error',
message,
{
at: new Date().toISOString(),
ok: false,
checked: 'nic',
status: null,
message,
detail: null,
request: null,
},
[tenantId],
);
return res.json({ ok: false, checked: 'nic', message, baseUrl: target.baseUrl });
}
const path = service.verifyPath ?? '/health';
const checked = service.verifyPath ? 'přístupové údaje' : 'jen dostupnost služby';
// Zaloha pro pripad, ze se k volani vubec nedoslo a chyba tedy `request` nema.
const verifyUrl = `${target.baseUrl.replace(/\/+$/, '')}${path}`;
const redact = createRedactor(Object.values(target.headers));
const controller = new AbortController();
@@ -295,43 +315,94 @@ connectorsRouter.post('/:id/test', async (req, res) => {
});
const response = await http.get(path);
await setConnectorStatus(connector.id, 'ok', null, [tenantId]);
const message = service.verifyPath
? 'Napojení funguje, přístupové údaje jsou platné.'
: 'Služba odpovídá. Přístupové údaje se tímhle neověřily, služba na to nemá čtecí volání.';
const request = { method: 'GET', path, url: verifyUrl };
// Do historie patri i uspech. Bez nej se neda poznat, jestli konektor
// nesel nikdy, nebo prestal jit ve chvili, kdy se sahlo na udaje.
await setConnectorStatus(
connector.id,
'ok',
null,
{
at: new Date().toISOString(),
ok: true,
checked,
status: response.status,
message,
detail: null,
request,
},
[tenantId],
);
return res.json({
ok: true,
checked,
status: response.status,
request: { method: 'GET', path },
message: service.verifyPath
? 'Napojení funguje, přístupové údaje jsou platné.'
: 'Služba odpovídá. Přístupové údaje se tímhle neověřily, služba na to nemá čtecí volání.',
request,
message,
baseUrl: target.baseUrl,
});
} catch (err) {
// Cela odpoved sluzby, ne jen "HTTP 401". Duvod je napsany prave v ni.
const isScriptError = err instanceof ScriptError;
const message = isScriptError ? redact(err.message) : redact(describe(err, 400));
const status = isScriptError ? err.status : undefined;
const detail = isScriptError
? err.detail
? redact(err.detail)
: undefined
: redact(truncate(String(err instanceof Error ? err.stack ?? err.message : err), config.errorDetailBytes));
await setConnectorStatus(connector.id, 'error', message, [tenantId]);
console.warn(
`[connectors] test ${connector.id} selhal: ${message}` + (detail ? `
${detail}` : ''),
);
const request =
isScriptError && err.request ? err.request : { method: 'GET', path, url: verifyUrl };
const check: ConnectorCheck = {
at: new Date().toISOString(),
ok: false,
checked,
status: status ?? null,
message,
detail: detail ?? null,
request,
};
await setConnectorStatus(connector.id, 'error', message, check, [tenantId]);
console.warn(`[connectors] test ${connector.id} selhal: ${message}`);
// Neuspesne overeni neni chyba API, je to vysledek. Proto 200.
return res.json({
ok: false,
checked,
message,
...(isScriptError && err.status !== undefined ? { status: err.status } : {}),
request: isScriptError && err.request ? err.request : { method: 'GET', path },
...(status !== undefined ? { status } : {}),
request,
baseUrl: target.baseUrl,
...(detail ? { detail } : {}),
});
} finally {
clearTimeout(timer);
}
});
/**
* Historie overeni konektoru.
*
* Zvlast, ne v seznamu konektoru: kazdy zaznam nese cele telo odpovedi sluzby
* a u peti konektoru by to byly desitky kilobajtu, ktere nikdo necte.
* Nacte se az kdyz o to nekdo v portalu opravdu stoji.
*/
connectorsRouter.get('/:id/checks', async (req, res) => {
const tenantId = tenantOrDeny(req, res);
if (!tenantId) return;
const connector = await getConnector(req.params.id, [tenantId]);
if (!connector) {
return res.status(404).json({ error: 'not_found', message: 'Konektor neexistuje.' });
}
if (!serviceOrDeny(req, res, connector.serviceId, tenantId)) return;
return res.json({ items: connector.checks, limit: CHECK_HISTORY });
});