fix(courier): log XFX track callback rawBody and Content-Type
Capture raw request for courier track callbacks regardless of Content-Type and persist contentType/rawBody/query into log_third_party for empty-body diagnosis. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,8 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { loadAppConfig } from '@dukang/shared-types';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { json, urlencoded } from 'express';
|
||||
import { json, urlencoded, type NextFunction, type Request, type Response } from 'express';
|
||||
import { parse as parseQueryString } from 'node:querystring';
|
||||
import { AppModule } from './app.module';
|
||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||
@@ -12,6 +13,61 @@ import { preloadSystemConfigEnv } from './common/system-config/system-config.env
|
||||
import { AlertService } from './common/alert/alert.service';
|
||||
import { initSentryIfConfigured } from './integrations/sentry/sentry.bootstrap';
|
||||
|
||||
function isCourierTrackCallbackUrl(url?: string): boolean {
|
||||
if (!url) return false;
|
||||
const path = url.split('?')[0] ?? '';
|
||||
return (
|
||||
(path.includes('/callbacks/courier/') && path.endsWith('/track')) ||
|
||||
path.endsWith('/callbacks/delivery/track')
|
||||
);
|
||||
}
|
||||
|
||||
function parseCourierCallbackBody(
|
||||
rawText: string,
|
||||
contentType: string,
|
||||
): Record<string, unknown> {
|
||||
const ct = contentType.toLowerCase();
|
||||
const trimmed = rawText.trim();
|
||||
if (!trimmed) return {};
|
||||
|
||||
if (ct.includes('application/json') || trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
return parsed && typeof parsed === 'object'
|
||||
? (parsed as Record<string, unknown>)
|
||||
: { value: parsed };
|
||||
} catch {
|
||||
return { _rawText: rawText.slice(0, 4000) };
|
||||
}
|
||||
}
|
||||
|
||||
if (ct.includes('application/x-www-form-urlencoded') || /[=&]/.test(trimmed)) {
|
||||
return parseQueryString(trimmed) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return { _rawText: rawText.slice(0, 4000) };
|
||||
}
|
||||
|
||||
/** 小飞侠回调:无论 Content-Type,先吃下 rawBody 再尽力解析(便于第三方日志排查) */
|
||||
function courierTrackRawBodyMiddleware(req: Request, _res: Response, next: NextFunction) {
|
||||
if (req.method !== 'POST' || !isCourierTrackCallbackUrl(req.originalUrl || req.url)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', (chunk: Buffer | string) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
});
|
||||
req.on('error', (err) => next(err));
|
||||
req.on('end', () => {
|
||||
const buf = Buffer.concat(chunks);
|
||||
const rawText = buf.toString('utf8');
|
||||
(req as Request & { rawBody?: Buffer }).rawBody = buf;
|
||||
req.body = parseCourierCallbackBody(rawText, String(req.headers['content-type'] || ''));
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const preloaded = await preloadSystemConfigEnv().catch((e) => {
|
||||
console.warn('[config] system_config preload skipped:', e instanceof Error ? e.message : e);
|
||||
@@ -26,9 +82,14 @@ async function bootstrap() {
|
||||
app.setGlobalPrefix('api/v1');
|
||||
app.set('trust proxy', true);
|
||||
app.enableCors({ origin: true, credentials: true });
|
||||
// 微信支付等需 rawBody;小飞侠回调多为 x-www-form-urlencoded
|
||||
// 小飞侠路由回调:优先捕获 rawBody(Content-Type 异常时也能入第三方日志)
|
||||
app.use(courierTrackRawBodyMiddleware);
|
||||
// 微信支付等需 rawBody;其它 JSON / form 请求走常规解析(跳过已由上面吃掉 body 的小飞侠回调)
|
||||
app.use(
|
||||
json({
|
||||
type: (req) =>
|
||||
!isCourierTrackCallbackUrl(req.originalUrl || req.url) &&
|
||||
Boolean(req.headers['content-type']?.includes('json')),
|
||||
verify: (req, _res, buf) => {
|
||||
if (
|
||||
req.url?.includes('/callbacks/wechat/pay') ||
|
||||
@@ -40,7 +101,14 @@ async function bootstrap() {
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.use(urlencoded({ extended: true }));
|
||||
app.use(
|
||||
urlencoded({
|
||||
extended: true,
|
||||
type: (req) =>
|
||||
!isCourierTrackCallbackUrl(req.originalUrl || req.url) &&
|
||||
Boolean(req.headers['content-type']?.includes('urlencoded')),
|
||||
}),
|
||||
);
|
||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
||||
app.useGlobalFilters(new HttpExceptionFilter(app.get(AlertService)));
|
||||
app.useGlobalInterceptors(new ResponseInterceptor(), app.get(LoggingInterceptor));
|
||||
|
||||
Reference in New Issue
Block a user