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:
@@ -6,6 +6,13 @@ import { logCourierCall } from '../integrations/courier/courier-log.util';
|
||||
import { TradeService } from '../modules/trade/trade.service';
|
||||
|
||||
const XFX_PROVIDER_ALIASES = new Set(['xfx', 'xiaofeixia']);
|
||||
const RAW_BODY_LOG_LIMIT = 4000;
|
||||
|
||||
export type TrackCallbackRequestMeta = {
|
||||
contentType?: string | null;
|
||||
rawBody?: string | null;
|
||||
query?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DeliveryCallbackService {
|
||||
@@ -16,15 +23,17 @@ export class DeliveryCallbackService {
|
||||
private readonly tradeService: TradeService,
|
||||
) {}
|
||||
|
||||
async handleTrackCallback(providerKey: string, body: unknown, requestUrl: string) {
|
||||
async handleTrackCallback(
|
||||
providerKey: string,
|
||||
body: unknown,
|
||||
requestUrl: string,
|
||||
meta?: TrackCallbackRequestMeta,
|
||||
) {
|
||||
const normalized = providerKey.trim().toLowerCase();
|
||||
const baseLog = {
|
||||
scene: 'TRACK_CALLBACK',
|
||||
requestUrl,
|
||||
requestBody: (body && typeof body === 'object' ? body : { value: body }) as Record<
|
||||
string,
|
||||
unknown
|
||||
>,
|
||||
requestBody: this.buildRequestBodyForLog(body, meta),
|
||||
};
|
||||
|
||||
if (!XFX_PROVIDER_ALIASES.has(normalized) && normalized !== 'logistics') {
|
||||
@@ -105,4 +114,58 @@ export class DeliveryCallbackService {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/** 第三方日志:保留解析后字段,并附带 Content-Type / rawBody / query 便于排查空 body */
|
||||
private buildRequestBodyForLog(
|
||||
body: unknown,
|
||||
meta?: TrackCallbackRequestMeta,
|
||||
): Record<string, unknown> {
|
||||
const parsed =
|
||||
body && typeof body === 'object'
|
||||
? ({ ...(body as Record<string, unknown>) } as Record<string, unknown>)
|
||||
: body === undefined || body === null
|
||||
? {}
|
||||
: { value: body };
|
||||
|
||||
const rawBody = meta?.rawBody != null ? this.redactSecrets(String(meta.rawBody)) : null;
|
||||
const query =
|
||||
meta?.query && Object.keys(meta.query).length > 0
|
||||
? this.redactSecretFields({ ...meta.query })
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...this.redactSecretFields(parsed),
|
||||
_meta: {
|
||||
contentType: meta?.contentType ?? null,
|
||||
rawBody:
|
||||
rawBody && rawBody.length > RAW_BODY_LOG_LIMIT
|
||||
? `${rawBody.slice(0, RAW_BODY_LOG_LIMIT)}…(truncated)`
|
||||
: rawBody,
|
||||
rawBodyLength: meta?.rawBody != null ? Buffer.byteLength(meta.rawBody, 'utf8') : 0,
|
||||
query,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private redactSecretFields(input: Record<string, unknown>): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (/^sign$/i.test(key) || /api[_-]?key/i.test(key)) {
|
||||
out[key] = '[REDACTED]';
|
||||
} else if (key === 'mchId' && value != null) {
|
||||
const s = String(value);
|
||||
out[key] = s.length <= 4 ? '****' : `${s.slice(0, 4)}****`;
|
||||
} else {
|
||||
out[key] = value;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private redactSecrets(raw: string): string {
|
||||
return raw
|
||||
.replace(/(sign=)[^&\s]*/gi, '$1[REDACTED]')
|
||||
.replace(/("sign"\s*:\s*")[^"]*/gi, '$1[REDACTED]')
|
||||
.replace(/(api[_-]?key=)[^&\s]*/gi, '$1[REDACTED]');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user