Refactor request listeners to use async functions
This commit is contained in:
1 parent
58488ebc3d
commit
7b8a49a7ba
11 files changed
+91
-106
No files matched your search
@@ -32,6 +32,7 @@
|
|||||||
{ "allowExpressions": true }
|
{ "allowExpressions": true }
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
|
"@typescript-eslint/no-misused-promises": "error",
|
||||||
"require-atomic-updates": 0,
|
"require-atomic-updates": 0,
|
||||||
"no-prototype-builtins": 0
|
"no-prototype-builtins": 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ if (!cluster.worker) {
|
|||||||
exception: err,
|
exception: err,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
});
|
});
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop(false).then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|
||||||
const initPromise = db
|
const initPromise = db
|
||||||
@@ -111,13 +111,19 @@ if (!cluster.worker) {
|
|||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
initPromise.finally(() => {
|
initPromise.finally(() => {
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server
|
||||||
|
.stop(false)
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
initPromise.finally(() => {
|
initPromise.finally(() => {
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server
|
||||||
|
.stop(false)
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+1
-11
@@ -79,8 +79,6 @@ if (!cluster.worker) {
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stopping = false;
|
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
process.on("uncaughtException", (err) => {
|
||||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
logger.error({
|
logger.error({
|
||||||
@@ -88,19 +86,13 @@ if (!cluster.worker) {
|
|||||||
exception: err,
|
exception: err,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
});
|
});
|
||||||
stopping = true;
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|
||||||
const _listener = (req, res): void => {
|
|
||||||
if (stopping) res.setHeader("Connection", "close");
|
|
||||||
void listener(req, res);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initPromise = db
|
const initPromise = db
|
||||||
.connect()
|
.connect()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
server.start(options, _listener);
|
server.start(options, listener);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -109,14 +101,12 @@ if (!cluster.worker) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
stopping = true;
|
|
||||||
initPromise.finally(() => {
|
initPromise.finally(() => {
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
stopping = true;
|
|
||||||
initPromise.finally(() => {
|
initPromise.finally(() => {
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-11
@@ -84,8 +84,6 @@ if (!cluster.worker) {
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stopping = false;
|
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
process.on("uncaughtException", (err) => {
|
||||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
logger.error({
|
logger.error({
|
||||||
@@ -93,19 +91,13 @@ if (!cluster.worker) {
|
|||||||
exception: err,
|
exception: err,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
});
|
});
|
||||||
stopping = true;
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|
||||||
const _listener = (req, res): void => {
|
|
||||||
if (stopping) res.setHeader("Connection", "close");
|
|
||||||
listener(req, res);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initPromise = db
|
const initPromise = db
|
||||||
.connect()
|
.connect()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
server.start(options, _listener);
|
server.start(options, listener);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -114,14 +106,12 @@ if (!cluster.worker) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
stopping = true;
|
|
||||||
initPromise.finally(() => {
|
initPromise.finally(() => {
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
stopping = true;
|
|
||||||
initPromise.finally(() => {
|
initPromise.finally(() => {
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-11
@@ -84,8 +84,6 @@ if (!cluster.worker) {
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stopping = false;
|
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
process.on("uncaughtException", (err) => {
|
||||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
logger.error({
|
logger.error({
|
||||||
@@ -93,19 +91,13 @@ if (!cluster.worker) {
|
|||||||
exception: err,
|
exception: err,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
});
|
});
|
||||||
stopping = true;
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|
||||||
const _listener = (req, res): void => {
|
|
||||||
if (stopping) res.setHeader("Connection", "close");
|
|
||||||
listener(req, res);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initPromise = db
|
const initPromise = db
|
||||||
.connect()
|
.connect()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
server.start(options, _listener);
|
server.start(options, async (req, res) => listener(req, res));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -114,14 +106,12 @@ if (!cluster.worker) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
stopping = true;
|
|
||||||
initPromise.finally(() => {
|
initPromise.finally(() => {
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
stopping = true;
|
|
||||||
initPromise.finally(() => {
|
initPromise.finally(() => {
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|||||||
+46
-43
@@ -59,6 +59,7 @@ import { parseXmlDeclaration } from "./xml-parser";
|
|||||||
import * as debug from "./debug";
|
import * as debug from "./debug";
|
||||||
import { getRequestOrigin } from "./forwarded";
|
import { getRequestOrigin } from "./forwarded";
|
||||||
import { getSocketEndpoints } from "./server";
|
import { getSocketEndpoints } from "./server";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
const gzipPromisified = promisify(zlib.gzip);
|
const gzipPromisified = promisify(zlib.gzip);
|
||||||
const deflatePromisified = promisify(zlib.deflate);
|
const deflatePromisified = promisify(zlib.deflate);
|
||||||
@@ -889,9 +890,33 @@ async function sendAcsRequest(
|
|||||||
return writeResponse(sessionContext, res);
|
return writeResponse(sessionContext, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function once(
|
||||||
|
emitter: EventEmitter,
|
||||||
|
event: string,
|
||||||
|
timeout: number
|
||||||
|
): Promise<unknown[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error(`Event ${event} timed out after ${timeout} ms`));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
emitter.once(event, (...args: unknown[]) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(args);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTimeoutPromise(delay: number, ref = true): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const timerId = setTimeout(resolve, delay);
|
||||||
|
if (!ref) timerId.unref();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// When socket closes, store active sessions in cache
|
// When socket closes, store active sessions in cache
|
||||||
export function onConnection(socket: Socket): void {
|
export async function onConnection(socket: Socket): Promise<void> {
|
||||||
socket.on("close", async () => {
|
await once(socket, "close", 300000);
|
||||||
const sessionContext = currentSessions.get(socket);
|
const sessionContext = currentSessions.get(socket);
|
||||||
if (!sessionContext) return;
|
if (!sessionContext) return;
|
||||||
currentSessions.delete(socket);
|
currentSessions.delete(socket);
|
||||||
@@ -922,34 +947,26 @@ export function onConnection(socket: Socket): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(async () => {
|
await cache.set(
|
||||||
const sessionContextString = await cache.get(
|
`session_${sessionContext.sessionId}`,
|
||||||
`session_${sessionContext.sessionId}`
|
await session.serialize(sessionContext),
|
||||||
|
Math.ceil(timeout / 1000) + 3
|
||||||
);
|
);
|
||||||
if (!sessionContextString) return;
|
|
||||||
const _sessionContext = await session.deserialize(sessionContextString);
|
await setTimeoutPromise(timeout + 1000, false);
|
||||||
|
const sessionStr = await cache.get(`session_${sessionContext.sessionId}`);
|
||||||
|
if (!sessionStr) return;
|
||||||
|
|
||||||
|
const _sessionContext = await session.deserialize(sessionStr);
|
||||||
if (_sessionContext.lastActivity === lastActivity) {
|
if (_sessionContext.lastActivity === lastActivity) {
|
||||||
logger.accessError(timeoutMsg);
|
logger.accessError(timeoutMsg);
|
||||||
await endSession(sessionContext);
|
await endSession(sessionContext);
|
||||||
}
|
}
|
||||||
}, timeout + 1000).unref();
|
|
||||||
|
|
||||||
if (sessionContext.state === 0) return;
|
|
||||||
|
|
||||||
const sessionContextString = await session.serialize(sessionContext);
|
|
||||||
await cache.set(
|
|
||||||
`session_${sessionContext.sessionId}`,
|
|
||||||
sessionContextString,
|
|
||||||
Math.ceil(timeout / 1000) + 3
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onClientError(err: Error, socket: Socket): void {
|
export async function onClientError(err: Error, socket: Socket): Promise<void> {
|
||||||
const remoteAddress = getSocketEndpoints(socket).remoteAddress;
|
const remoteAddress = getSocketEndpoints(socket).remoteAddress;
|
||||||
localCache
|
const cacheSnapshot = await localCache.getCurrentSnapshot();
|
||||||
.getCurrentSnapshot()
|
|
||||||
.then((cacheSnapshot) => {
|
|
||||||
const debugEnabled = !!localCache.getConfig(
|
const debugEnabled = !!localCache.getConfig(
|
||||||
cacheSnapshot,
|
cacheSnapshot,
|
||||||
"cwmp.debug",
|
"cwmp.debug",
|
||||||
@@ -965,10 +982,6 @@ export function onClientError(err: Error, socket: Socket): void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (debugEnabled) debug.clientError(remoteAddress, err);
|
if (debugEnabled) debug.clientError(remoteAddress, err);
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@@ -1341,29 +1354,19 @@ async function processRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listener(
|
export async function listener(
|
||||||
httpRequest: IncomingMessage,
|
httpRequest: IncomingMessage,
|
||||||
httpResponse: ServerResponse
|
httpResponse: ServerResponse
|
||||||
): void {
|
): Promise<void> {
|
||||||
stats.concurrentRequests += 1;
|
stats.concurrentRequests += 1;
|
||||||
listenerAsync(httpRequest, httpResponse)
|
|
||||||
.then(() => {
|
|
||||||
stats.concurrentRequests -= 1;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
currentSessions.delete(httpRequest.socket);
|
|
||||||
stats.concurrentRequests -= 1;
|
|
||||||
setTimeout(() => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
httpRequest.socket.unref();
|
await listenerAsync(httpRequest, httpResponse);
|
||||||
httpResponse.writeHead(500, { Connection: "close" });
|
|
||||||
httpResponse.end(`${err.name}: ${err.message}`);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore
|
currentSessions.delete(httpRequest.socket);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
stats.concurrentRequests -= 1;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clientError(
|
async function clientError(
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ onConnect(async (db) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export async function disconnect(): Promise<void> {
|
export async function disconnect(): Promise<void> {
|
||||||
if (clientPromise) await (await clientPromise).close();
|
if (clientPromise != null) await (await clientPromise).close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimize projection by removing overlaps
|
// Optimize projection by removing overlaps
|
||||||
|
|||||||
+5
-20
@@ -66,10 +66,10 @@ async function getBody(request: IncomingMessage): Promise<Buffer> {
|
|||||||
return Buffer.concat(chunks);
|
return Buffer.concat(chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listener(
|
export async function listener(
|
||||||
request: IncomingMessage,
|
request: IncomingMessage,
|
||||||
response: ServerResponse
|
response: ServerResponse
|
||||||
): void {
|
): Promise<void> {
|
||||||
response.setHeader("GenieACS-Version", VERSION);
|
response.setHeader("GenieACS-Version", VERSION);
|
||||||
|
|
||||||
const origin = getRequestOrigin(request);
|
const origin = getRequestOrigin(request);
|
||||||
@@ -78,14 +78,10 @@ export function listener(
|
|||||||
(origin.encrypted ? "https://" : "http://") + origin.host
|
(origin.encrypted ? "https://" : "http://") + origin.host
|
||||||
);
|
);
|
||||||
|
|
||||||
let readableEnded = false;
|
const body = await getBody(request).catch(() => null);
|
||||||
// For Node 12.9+ we can just use stream.readableEnded
|
// Ignore incomplete requests
|
||||||
request.on("end", () => {
|
if (body == null) return;
|
||||||
readableEnded = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
getBody(request)
|
|
||||||
.then((body) => {
|
|
||||||
logger.accessInfo(
|
logger.accessInfo(
|
||||||
Object.assign({}, Object.fromEntries(url.searchParams), {
|
Object.assign({}, Object.fromEntries(url.searchParams), {
|
||||||
remoteAddress: origin.remoteAddress,
|
remoteAddress: origin.remoteAddress,
|
||||||
@@ -93,17 +89,6 @@ export function listener(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
return handler(request, response, url, body);
|
return handler(request, response, url, body);
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// Ignore incomplete requests
|
|
||||||
if (!readableEnded) return;
|
|
||||||
|
|
||||||
if (!response.headersSent) {
|
|
||||||
response.writeHead(500, { Connection: "close" });
|
|
||||||
response.end(`${err.name}: ${err.message}`);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler(
|
async function handler(
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ function runExtension(sessionContext, key, extCall): Promise<Fault> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let prom = re.get(key);
|
let prom = re.get(key);
|
||||||
if (!prom) {
|
if (prom == null) {
|
||||||
re.set(
|
re.set(
|
||||||
key,
|
key,
|
||||||
(prom = new Promise((resolve, reject) => {
|
(prom = new Promise((resolve, reject) => {
|
||||||
|
|||||||
+24
-4
@@ -25,7 +25,8 @@ import * as path from "path";
|
|||||||
import { ROOT_DIR } from "./config";
|
import { ROOT_DIR } from "./config";
|
||||||
|
|
||||||
let server: http.Server | https.Server;
|
let server: http.Server | https.Server;
|
||||||
let listener: (...args) => void;
|
let listener: http.RequestListener;
|
||||||
|
let stopping = false;
|
||||||
|
|
||||||
function closeServer(timeout, callback): void {
|
function closeServer(timeout, callback): void {
|
||||||
if (!server) return void callback();
|
if (!server) return void callback();
|
||||||
@@ -73,11 +74,29 @@ interface SocketEndpoint {
|
|||||||
// Save this info as they're not accessible after a socket has been closed
|
// Save this info as they're not accessible after a socket has been closed
|
||||||
const socketEndpoints: WeakMap<Socket, SocketEndpoint> = new WeakMap();
|
const socketEndpoints: WeakMap<Socket, SocketEndpoint> = new WeakMap();
|
||||||
|
|
||||||
|
type Promisify<T extends (...args: any) => any> = (
|
||||||
|
...args: Parameters<T>
|
||||||
|
) => Promise<ReturnType<T>>;
|
||||||
|
|
||||||
export function start(
|
export function start(
|
||||||
options: ServerOptions,
|
options: ServerOptions,
|
||||||
_listener: http.RequestListener
|
_listener: Promisify<http.RequestListener>
|
||||||
): void {
|
): void {
|
||||||
listener = _listener;
|
listener = (req, res) => {
|
||||||
|
if (stopping) res.setHeader("Connection", "close");
|
||||||
|
_listener(req, res).catch((err) => {
|
||||||
|
try {
|
||||||
|
res.socket.unref();
|
||||||
|
if (res.headersSent) {
|
||||||
|
res.writeHead(500, { Connection: "close" });
|
||||||
|
res.end(`${err.name}: ${err.message}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (options.ssl) {
|
if (options.ssl) {
|
||||||
const opts = {
|
const opts = {
|
||||||
@@ -124,7 +143,8 @@ export function start(
|
|||||||
server.listen({ port: options.port, host: options.host });
|
server.listen({ port: options.port, host: options.host });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stop(): Promise<void> {
|
export function stop(terminateConnections = true): Promise<void> {
|
||||||
|
stopping = terminateConnections;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reject(new Error("Could not close server in a timely manner"));
|
reject(new Error("Could not close server in a timely manner"));
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const getAuthorizer = memoize(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
koa.on("error", async (err, ctx) => {
|
koa.on("error", (err, ctx) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Ignored errors resulting from aborted requests
|
// Ignored errors resulting from aborted requests
|
||||||
if (ctx?.req.aborted) return;
|
if (ctx?.req.aborted) return;
|
||||||
|
|||||||
Reference in new issue
Block a user