Dialog portalem do body, hlavicky odpovedi u chyby

Dialog se vykresloval uvnitr karty konektoru misto pres obrazovku. Samo
position: fixed nestaci: rodic s backdrop-filter (nase .glass, tedy skoro
kazdy panel a karta) je pro fixed potomka containing block. Modal proto jde
portalem do document.body. Tykalo se to vsech dialogu, videt to bylo az
u Logu, ktere jsou v male karte.

K chybe se zapisuji vybrane hlavicky odpovedi: server, via, content-type,
www-authenticate, retry-after, x-request-id, date. Rikaji, kdo odpoved vydal.
Server: Kestrel je sama aplikace, Via: 1.1 Caddy proxy pred ni. U 403 od proxy
byva telo prazdne a bez hlavicek by nezbylo vubec nic. Allowlist, ne vsechno:
Set-Cookie a podobne do zaznamu nepatri.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-25 07:15:22 +02:00
co-authored by Claude Opus 5
parent 5a124a53d8
commit ab88979627
11 changed files with 143 additions and 9 deletions
+5
View File
@@ -30,6 +30,11 @@ export interface ConnectorCheck {
detail: string | null;
/** Cela adresa vcetne serveru, bez query. null, kdyz se k volani nedoslo. */
request: { method: string; path: string; url: string } | null;
/**
* Vybrane hlavicky odpovedi. Rikaji, kdo odpoved vydal - aplikace, nebo
* proxy pred ni. U kodu bez tela je to jedina stopa, ktera zbyde.
*/
responseHeaders: Record<string, string> | null;
}
/** Kolik zaznamu o overeni se u konektoru drzi. */
+11
View File
@@ -1739,6 +1739,17 @@ export function buildOpenApiDocument() {
url: { type: 'string' },
},
},
responseHeaders: {
type: 'object',
nullable: true,
additionalProperties: { type: 'string' },
description:
'Vybrane hlavicky odpovedi (server, via, content-type, ' +
'www-authenticate, retry-after, x-request-id, date). Rikaji, kdo ' +
'odpoved vydal - aplikace, nebo proxy pred ni. U kodu bez tela ' +
'je to jedina stopa, ktera zbyde. Allowlist, ne vsechno: ' +
'Set-Cookie a podobne do zaznamu nepatri.',
},
},
},
},
+7
View File
@@ -289,6 +289,7 @@ connectorsRouter.post('/:id/test', async (req, res) => {
message,
detail: null,
request: null,
responseHeaders: null,
},
[tenantId],
);
@@ -334,6 +335,7 @@ connectorsRouter.post('/:id/test', async (req, res) => {
message,
detail: null,
request,
responseHeaders: null,
},
[tenantId],
);
@@ -359,6 +361,9 @@ connectorsRouter.post('/:id/test', async (req, res) => {
const request =
isScriptError && err.request ? err.request : { method: 'GET', path, url: verifyUrl };
// Hlavicky rikaji, kdo odpoved vydal. U 403 bez tela je to vsechno,
// co zbyde: `Server: Kestrel` je aplikace, `Via: 1.1 Caddy` proxy.
const responseHeaders = isScriptError ? (err.responseHeaders ?? null) : null;
const check: ConnectorCheck = {
at: new Date().toISOString(),
ok: false,
@@ -367,6 +372,7 @@ connectorsRouter.post('/:id/test', async (req, res) => {
message,
detail: detail ?? null,
request,
responseHeaders,
};
await setConnectorStatus(connector.id, 'error', message, check, [tenantId]);
@@ -380,6 +386,7 @@ connectorsRouter.post('/:id/test', async (req, res) => {
...(status !== undefined ? { status } : {}),
request,
baseUrl: target.baseUrl,
...(responseHeaders ? { responseHeaders } : {}),
...(detail ? { detail } : {}),
});
} finally {
+39 -3
View File
@@ -102,6 +102,33 @@ const reasonListKeys = ['missingHeaders', 'errors', 'Errors'];
/** Strop na duvod v hlasce. Cele telo zustava v `detail`, tohle je jen veta. */
const reasonBytes = 400;
/**
* Hlavicky odpovedi, ktere se zapisuji k chybe.
*
* Allowlist, ne vsechno: v odpovedi muze byt `Set-Cookie` nebo token a ten
* do zaznamu nepatri. Tyhle rikaji, **kdo** odpoved vydal - `Server: Kestrel`
* je sama aplikace, `Via: 1.1 Caddy` proxy pred ni. U 403 bez tela je to
* jedina stopa, ktera zbyde.
*/
const keptResponseHeaders = [
'server',
'via',
'content-type',
'www-authenticate',
'retry-after',
'x-request-id',
'date',
];
function pickResponseHeaders(response: Response): Record<string, string> {
const picked: Record<string, string> = {};
for (const name of keptResponseHeaders) {
const value = response.headers.get(name);
if (value !== null && value !== '') picked[name] = truncate(value, 200);
}
return picked;
}
function reasonFromText(value: string, depth: number): string | null {
const raw = value.trim();
if (raw === '') return null;
@@ -183,13 +210,17 @@ function statusError(
status: number,
request: ScriptRequestInfo,
detail: string,
responseHeaders: Record<string, string>,
): ScriptError {
const where = `${request.method} ${request.url} vrátilo HTTP ${status}`;
const reason = reasonFromBody(detail);
// Kdyz telo nic nerika, aspon se rekne, kdo odpoved vydal. Prazdne telo
// u 403 obvykle znamena, ze to nevydala aplikace, ale neco pred ni.
const who = responseHeaders.server ?? responseHeaders.via;
const said = reason
? ` Služba odpověděla: ${reason}`
: ' Služba k tomu nenapsala nic, tělo odpovědi je prázdné.';
const options = { status, detail, request };
: ` Tělo odpovědi je prázdné${who ? `, odpověď vydal ${who}` : ''}.`;
const options = { status, detail, request, responseHeaders };
if (retryableStatuses.has(status)) {
return new ScriptError('retryable', `Služba je momentálně nedostupná: ${where}.${said}`, options);
@@ -339,7 +370,12 @@ export function createHttp(options: CreateHttpOptions): ScriptHttp {
if (!response.ok && !allowed.includes(response.status)) {
// Zamerne surove telo, ne prochazene pres JSON.stringify - u chyby chceme
// presne to, co sluzba poslala, vcetne pripadneho HTML nebo textu.
throw statusError(response.status, request, redact(truncate(raw, config.errorDetailBytes)));
throw statusError(
response.status,
request,
redact(truncate(raw, config.errorDetailBytes)),
pickResponseHeaders(response),
);
}
return { status: response.status, body: parsed as T };
+10
View File
@@ -185,6 +185,14 @@ export class ScriptError extends Error {
*/
readonly detail?: string;
readonly request?: ScriptRequestInfo;
/**
* Vybrane hlavicky odpovedi. Rikaji, **kdo** odpoved vydal: `Server: Kestrel`
* je aplikace, `Via: 1.1 Caddy` sama proxy. U kodu bez tela (a 403 od proxy
* telo casto nema) je to jedina stopa, ktera zbyde.
*
* Allowlist, ne vsechno: `Set-Cookie` a podobne do zaznamu nepatri.
*/
readonly responseHeaders?: Record<string, string>;
constructor(
kind: ScriptErrorKind,
@@ -193,6 +201,7 @@ export class ScriptError extends Error {
status?: number;
detail?: string;
request?: ScriptRequestInfo;
responseHeaders?: Record<string, string>;
cause?: unknown;
} = {},
) {
@@ -202,6 +211,7 @@ export class ScriptError extends Error {
this.status = options.status;
this.detail = options.detail;
this.request = options.request;
this.responseHeaders = options.responseHeaders;
}
}