perf: Check socket is still connected before querying db (#11620)

This commit is contained in:
Tom Moor
2026-03-02 17:47:06 -05:00
committed by GitHub
parent 904f1a9726
commit c428d551b8
4 changed files with 42 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
import type { Next } from "koa";
import type { AppContext } from "@server/types";
import { requestContext } from "@server/storage/requestContext";
/**
* Middleware that wraps the request in an AsyncLocalStorage context, making the
* current request available to Sequelize hooks so that queries can be
* short-circuited when the socket has been destroyed (e.g. after a timeout).
*
* @returns The middleware function.
*/
export default function requestContextMiddleware() {
return (ctx: AppContext, next: Next) =>
requestContext.run({ req: ctx.req }, next);
}
+2
View File
@@ -8,6 +8,7 @@ import env from "@server/env";
import { NotFoundError } from "@server/errors";
import { apiContext } from "@server/middlewares/apiContext";
import coalesceBody from "@server/middlewares/coaleseBody";
import requestContextMiddleware from "@server/middlewares/requestContext";
import requestTracer from "@server/middlewares/requestTracer";
import { verifyCSRFToken } from "@server/middlewares/csrf";
import type { AppState, AppContext } from "@server/types";
@@ -55,6 +56,7 @@ const api = new Koa<AppState, AppContext>();
const router = new Router();
// middlewares
api.use(requestContextMiddleware());
api.use(
bodyParser({
multipart: true,
+14
View File
@@ -6,9 +6,11 @@ import { Sequelize } from "sequelize-typescript";
import type { MigrationError } from "umzug";
import { Umzug, SequelizeStorage } from "umzug";
import env from "@server/env";
import { ClientClosedRequestError } from "@server/errors";
import type Model from "@server/models/base/Model";
import Logger from "../logging/Logger";
import * as models from "../models";
import { requestContext } from "./requestContext";
import { getConnectionName } from "./utils";
/**
@@ -107,6 +109,18 @@ export function createDatabaseInstance(
instance = monkeyPatchSequelizeErrorsForJest(instance);
}
// Skip queries when the originating HTTP request socket has been destroyed
// (e.g. client disconnected or server timeout). This avoids wasting database
// resources on work whose response can never be delivered.
const assertConnectionOpen = () => {
const store = requestContext.getStore();
if (store?.req.socket.destroyed) {
throw ClientClosedRequestError();
}
};
instance.addHook("beforeFind", assertConnectionOpen);
instance.addHook("beforeCount", assertConnectionOpen);
// Add hooks to warn about write operations on read-only connections
if (isReadOnly) {
const warnWriteOperation = (operation: string) => {
+11
View File
@@ -0,0 +1,11 @@
import { AsyncLocalStorage } from "node:async_hooks";
import type { IncomingMessage } from "node:http";
/**
* Async local storage for the current HTTP request context. This allows
* downstream code (e.g. Sequelize hooks) to check whether the originating
* request is still alive without explicitly threading `ctx` through every call.
*/
export const requestContext = new AsyncLocalStorage<{
req: IncomingMessage;
}>();