fix(api): уточнить причины отказа входа на маршрутизатор

This commit is contained in:
Denozordec committed 2026-10-16 01:32:29 +07:00
1 parent 552d5f831f
commit 25ae76dc3f
5 files changed
+128 -6

No files matched your search

+2 -4
View File
@@ -12,7 +12,7 @@ import {
type TestConnectionRequest,
} from "../types/server.js"
import { pollServer } from "../services/poller.js"
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
import { MikrotikClient, MikrotikError, routerOsAccessHint } from "../services/mikrotik.js"
import { resolveRosSrcIpv4 } from "../utils/ros-src-address.js"
import {
createServer,
@@ -56,9 +56,7 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
let message = "Не удалось подключиться"
if (err instanceof MikrotikError) {
if (err.statusCode === 401) message = "Неверный логин или пароль (401 Unauthorized)"
else if (err.statusCode === 403) message = "Нет прав доступа к REST API (403 Forbidden)"
else message = `Ошибка RouterOS API: ${err.statusCode}`
message = routerOsAccessHint(err.statusCode, err.body) ?? `Ошибка RouterOS API: ${err.statusCode}`
} else if (err instanceof Error) {
const msg = err.message.toLowerCase()
if (msg.includes("timeout") || msg.includes("abort"))
+29 -1
View File
@@ -1,7 +1,35 @@
import { test } from "node:test"
import assert from "node:assert/strict"
import { createServer } from "node:http"
import { MikrotikClient } from "./mikrotik.js"
import { MikrotikClient, MikrotikError, routerOsAccessHint } from "./mikrotik.js"
test("RouterOS access diagnostics distinguish login saturation without assuming a wrong password or retrying", async () => {
let requests = 0
const body = '{"detail":"too many concurrent logins (6)","error":500,"message":"Internal Server Error"}'
const server = createServer((req, res) => { requests++; req.resume(); res.writeHead(500); res.end(body) })
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
const address = server.address(); assert(address && typeof address !== "string")
const client = new MikrotikClient({ host: "127.0.0.1", port: address.port, useSsl: false, verifySsl: false, username: "fixture", password: "fixture" })
try {
for (const operation of [() => client.get("/interface"), () => client.put("/own", {})]) {
await assert.rejects(operation, (error: unknown) => {
assert(error instanceof MikrotikError)
assert.equal(error.statusCode, 500); assert.equal(error.body, body)
assert.match(error.message, /превышен лимит одновременных входов/)
return true
})
}
assert.equal(requests, 2, "Neither reads nor writes may silently retry a refused login")
assert.match(routerOsAccessHint(401, '{"message":"Unauthorized"}')!, /ограничения входа/)
assert.doesNotMatch(routerOsAccessHint(401, "")!, /Неверный логин или пароль/)
for (const unknown of ["not JSON", "null", '{"detail":6}', '{"detail":"disk failure"}']) {
assert.equal(routerOsAccessHint(500, unknown), undefined)
}
assert.equal(routerOsAccessHint(400, body), undefined)
} finally {
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
}
})
test("RouterOS print preserves native query arrays and property selection", async () => {
let received: unknown, target = "", method = ""
+17 -1
View File
@@ -1110,13 +1110,29 @@ export class MikrotikClient {
// ── Error type ─────────────────────────────────────────────────────────────────
/** HTTP 401 alone does not distinguish wrong credentials from a refused login. */
export function routerOsAccessHint(statusCode: number, body: string): string | undefined {
if (statusCode === 401) return "RouterOS отклонил авторизацию: проверьте учётные данные, права API/REST и ограничения входа"
if (statusCode === 403) return "RouterOS запретил доступ к REST API"
if (statusCode === 500) {
try {
const detail: unknown = JSON.parse(body)?.detail
if (typeof detail === "string" && /^too many concurrent logins(?:\s*\(\d+\))?$/i.test(detail.trim())) {
return "RouterOS занят: превышен лимит одновременных входов"
}
} catch { /* Keep an unrecognized response unchanged. */ }
}
return undefined
}
export class MikrotikError extends Error {
constructor(
public readonly statusCode: number,
public readonly path: string,
public readonly body: string,
) {
super(`RouterOS API error ${statusCode} on ${path}: ${body}`)
const hint = routerOsAccessHint(statusCode, body)
super(`${hint ? `${hint}. ` : ""}RouterOS API error ${statusCode} on ${path}: ${body}`)
this.name = "MikrotikError"
}
}