Skip to content

Commit 2fff823

Browse files
committed
fix(reqLogger): prevent destroyed socket race
1 parent 8d54b18 commit 2fff823

File tree

1 file changed

+83
-47
lines changed

1 file changed

+83
-47
lines changed

src/telemetry/requestLogger.ts

Lines changed: 83 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
import type { RequestHandler, Request, Response, ErrorRequestHandler } from 'express';
2-
import { getClientIp } from 'request-ip';
3-
import type { Histogram } from '@opentelemetry/api';
4-
import cleanStack from 'clean-stack';
1+
import type {
2+
RequestHandler,
3+
Request,
4+
Response,
5+
ErrorRequestHandler,
6+
} from "express";
7+
import { getClientIp } from "request-ip";
8+
import type { Histogram } from "@opentelemetry/api";
9+
import cleanStack from "clean-stack";
510

6-
import { ServiceError } from '../error.js';
7-
import type { AnyServiceLocals, RequestWithApp, ServiceExpress, ServiceLocals } from '../types.js';
8-
import type { ServiceHandler } from '../express-app/types.js';
9-
import type { ConfigurationSchema } from '../config/schema.js';
10-
import { getNodeEnv } from '../env.js';
11+
import { ServiceError } from "../error.js";
12+
import type {
13+
AnyServiceLocals,
14+
RequestWithApp,
15+
ServiceExpress,
16+
ServiceLocals,
17+
} from "../types.js";
18+
import type { ServiceHandler } from "../express-app/types.js";
19+
import type { ConfigurationSchema } from "../config/schema.js";
20+
import { getNodeEnv } from "../env.js";
1121

12-
const LOG_PREFS = Symbol('Logging information');
13-
const LOGGED_SEMAPHORE = Symbol('Logged semaphore');
22+
const LOG_PREFS = Symbol("Logging information");
23+
const LOGGED_SEMAPHORE = Symbol("Logged semaphore");
24+
const SUCCESS_SEMAPHORE = Symbol("Response finished semaphore");
1425

1526
interface LogPrefs {
1627
start: [number, number];
@@ -38,12 +49,12 @@ function getBasicInfo(req: Request): [string, Record<string, string | number>] {
3849
const url = req.originalUrl || req.url;
3950

4051
const preInfo: Record<string, string> = {
41-
ip: getClientIp(req) || '',
52+
ip: getClientIp(req) || "",
4253
m: req.method,
4354
};
4455

45-
if (req.headers['user-agent']) {
46-
preInfo.ua = req.headers['user-agent'];
56+
if (req.headers["user-agent"]) {
57+
preInfo.ua = req.headers["user-agent"];
4758
}
4859

4960
const sessionReq = req as WithIdentifiedSession;
@@ -54,7 +65,9 @@ function getBasicInfo(req: Request): [string, Record<string, string | number>] {
5465
return [url, preInfo];
5566
}
5667

57-
function finishLog<SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>>(
68+
function finishLog<
69+
SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
70+
>(
5871
app: ServiceExpress<SLocals>,
5972
error: Error | undefined,
6073
req: Request,
@@ -80,20 +93,22 @@ function finishLog<SLocals extends AnyServiceLocals = ServiceLocals<Configuratio
8093
}
8194
const [url, preInfo] = getBasicInfo(req);
8295

83-
let responseType = 'finished';
96+
let responseType = "unknown";
8497

8598
// ts warning is known and incorrect—`aborted` is a subset of `destroyed`
86-
if (req.aborted) {
87-
responseType = 'aborted';
99+
if (res[SUCCESS_SEMAPHORE]) {
100+
responseType = "finished";
101+
} else if (req.aborted) {
102+
responseType = "aborted";
88103
} else if (req.destroyed) {
89-
responseType = 'destroyed';
104+
responseType = "destroyed";
90105
} else if (error) {
91-
responseType = 'errored';
106+
responseType = "errored";
92107
}
93108

94109
const endLog: Record<string, string | string[] | number | undefined> = {
95110
...preInfo,
96-
t: 'req',
111+
t: "req",
97112
r: responseType,
98113
s: (error as ErrorWithStatus)?.status || res.statusCode || 0,
99114
dur,
@@ -125,21 +140,22 @@ function finishLog<SLocals extends AnyServiceLocals = ServiceLocals<Configuratio
125140
if (prefs.logRequests) {
126141
endLog.h = JSON.stringify(req.headers);
127142
if (Buffer.isBuffer(req.body)) {
128-
endLog.b = req.body.toString('base64');
129-
} else if (typeof req.body !== 'string') {
143+
endLog.b = req.body.toString("base64");
144+
} else if (typeof req.body !== "string") {
130145
endLog.b = JSON.stringify(req.body);
131146
} else if (req.body) {
132147
endLog.b = req.body;
133148
}
134149
}
135150

136151
if (prefs.chunks?.length) {
137-
const bodyString = Buffer.concat(prefs.chunks).toString('utf8');
152+
const bodyString = Buffer.concat(prefs.chunks).toString("utf8");
138153
if (bodyString) {
139154
endLog.resBody = bodyString;
140155
}
141156
}
142-
const msg = service.getLogFields?.(req as RequestWithApp<SLocals>, endLog) || url;
157+
const msg =
158+
service.getLogFields?.(req as RequestWithApp<SLocals>, endLog) || url;
143159
if (unexpectedError) {
144160
logger.error(endLog, msg);
145161
} else {
@@ -154,14 +170,17 @@ export function loggerMiddleware<
154170
>(
155171
app: ServiceExpress<SLocals>,
156172
histogram: Histogram,
157-
config?: ConfigurationSchema['logging'],
173+
config?: ConfigurationSchema["logging"],
158174
): RequestHandler {
159-
const nonProd = getNodeEnv() !== 'production';
175+
const nonProd = getNodeEnv() !== "production";
160176
const { logger, service } = app.locals;
161177
return function serviceLogger(req, res, next) {
162178
const logResponse =
163-
config?.logResponseBody || (nonProd && req.headers['x-log']?.includes('res'));
164-
const logRequest = config?.logRequestBody || (nonProd && req.headers['x-log']?.includes('req'));
179+
config?.logResponseBody ||
180+
(nonProd && req.headers["x-log"]?.includes("res"));
181+
const logRequest =
182+
config?.logRequestBody ||
183+
(nonProd && req.headers["x-log"]?.includes("req"));
165184
const prefs: LogPrefs = {
166185
start: process.hrtime(),
167186
logRequests: logRequest,
@@ -176,44 +195,57 @@ export function loggerMiddleware<
176195
// data is to monkey-patch.
177196
const oldWrite = res.write;
178197
const oldEnd = res.end;
179-
res.write = ((...args: Parameters<(typeof res)['write']>) => {
198+
res.write = ((...args: Parameters<(typeof res)["write"]>) => {
180199
if (prefs.chunks) {
181-
prefs.chunks.push(Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0] as string));
200+
prefs.chunks.push(
201+
Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0] as string),
202+
);
182203
}
183-
return (oldWrite as (typeof res)['write']).apply(res, args);
184-
}) as (typeof res)['write'];
185-
res.end = ((...args: Parameters<(typeof res)['end']>) => {
204+
return (oldWrite as (typeof res)["write"]).apply(res, args);
205+
}) as (typeof res)["write"];
206+
res.end = ((...args: Parameters<(typeof res)["end"]>) => {
186207
if (args[0] && prefs.chunks) {
187-
prefs.chunks.push(Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0] as string));
208+
prefs.chunks.push(
209+
Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0] as string),
210+
);
188211
}
189212
return oldEnd.apply(res, args);
190-
}) as (typeof res)['end'];
213+
}) as (typeof res)["end"];
191214
}
192215

193216
if (config?.preLog) {
194217
const [url, preInfo] = getBasicInfo(req);
195218
const preLog: Record<string, string | string[] | number | undefined> = {
196219
...preInfo,
197-
t: 'pre',
220+
t: "pre",
198221
ref: req.headers.referer || undefined,
199222
sid: (req as WithIdentifiedSession).session?.id,
200223
c: req.headers.correlationid || undefined,
201224
};
202-
const msg = service.getLogFields?.(req as RequestWithApp<SLocals>, preLog) || url;
225+
const msg =
226+
service.getLogFields?.(req as RequestWithApp<SLocals>, preLog) || url;
203227
logger.info(preLog, msg);
204228
}
205229

206230
const logWriter = (err?: Error) => finishLog(app, err, req, res, histogram);
207-
res.on('finish', logWriter);
208-
res.on('close', logWriter);
209-
res.on('error', logWriter);
231+
res.on("finish", (err?: Error) => {
232+
res[SUCCESS_SEMAPHORE] = true;
233+
logWriter(err);
234+
});
235+
res.on("close", logWriter);
236+
res.on("error", logWriter);
210237
next();
211238
};
212239
}
213240

214241
export function errorHandlerMiddleware<
215242
SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
216-
>(app: ServiceExpress<SLocals>, histogram: Histogram, unnest?: boolean, returnError?: boolean) {
243+
>(
244+
app: ServiceExpress<SLocals>,
245+
histogram: Histogram,
246+
unnest?: boolean,
247+
returnError?: boolean,
248+
) {
217249
const svcErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
218250
let loggable: Partial<ServiceError> = error;
219251
const body = error.response?.body || error.body;
@@ -249,11 +281,15 @@ export function errorHandlerMiddleware<
249281

250282
export function notFoundMiddleware() {
251283
const serviceNotFoundHandler: ServiceHandler = (req, res, next) => {
252-
const error = new ServiceError(req.app, `Cannot ${req.method} ${req.path}`, {
253-
status: 404,
254-
code: 'NotFound',
255-
domain: 'http',
256-
});
284+
const error = new ServiceError(
285+
req.app,
286+
`Cannot ${req.method} ${req.path}`,
287+
{
288+
status: 404,
289+
code: "NotFound",
290+
domain: "http",
291+
},
292+
);
257293
next(error);
258294
};
259295
return serviceNotFoundHandler as RequestHandler;

0 commit comments

Comments
 (0)