🔧 chore: 为 BFF 代理和 GraphQL 函数添加调试日志以排查 500 错误

- Edge Function (bff-proxy): 添加请求入口、ACCESS_KEY 检查、body 读取、
  转发目标、fetch 结果等关键节点日志
- Function (giffgaff-graphql): 添加操作名、参数完整性、上游调用状态、
  token 刷新链路等全链路日志
- 修复: fetch 异常时不再向客户端暴露内部错误详情(安全加固)
- 所有日志仅记录布尔态和状态码,不泄露 token/签名等敏感信息
This commit is contained in:
Abner
2026-05-19 12:13:57 +08:00
parent 51b268d96d
commit 8559563e39
3 changed files with 71 additions and 23 deletions

View File

@@ -7,6 +7,7 @@
export default async (request, context) => {
const url = new URL(request.url);
const ts = new Date().toISOString();
// 仅处理 /bff/* 路径
if (!url.pathname.startsWith('/bff/')) {
@@ -18,6 +19,8 @@ export default async (request, context) => {
return new Response('Bad Request', { status: 400 });
}
console.log(`[BFF] ${ts} | ${request.method} ${url.pathname} → target=${targetName}`);
// 目标 Netlify Function URL同域
const functionUrl = new URL(`/.netlify/functions/${targetName}` , request.url);
@@ -25,11 +28,13 @@ export default async (request, context) => {
// Netlify Edge 使用 Deno 运行时
const accessKey = (typeof Deno !== 'undefined' && Deno.env && Deno.env.get('ACCESS_KEY')) || '';
if (!accessKey) {
console.error(`[BFF] ${ts} | ACCESS_KEY missing in Edge env`);
return new Response(JSON.stringify({ error: 'Server Misconfigured', message: 'ACCESS_KEY not configured' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
console.log(`[BFF] ${ts} | ACCESS_KEY present, proceeding`);
// 复制请求头并添加服务端密钥头(仅内部互调使用,不影响浏览器 CORS
const headers = new Headers(request.headers);
@@ -41,7 +46,16 @@ export default async (request, context) => {
let body = undefined;
const isMutating = request.method !== 'GET' && request.method !== 'HEAD';
if (isMutating) {
body = await request.arrayBuffer();
try {
body = await request.arrayBuffer();
console.log(`[BFF] ${ts} | body read OK, size=${body.byteLength} bytes`);
} catch (bodyErr) {
console.error(`[BFF] ${ts} | body read FAILED: ${bodyErr.message}`);
return new Response(JSON.stringify({ error: 'Bad Request', message: 'Failed to read request body' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
}
const proxiedRequest = new Request(functionUrl.toString(), {
@@ -50,14 +64,20 @@ export default async (request, context) => {
body
});
const response = await fetch(proxiedRequest);
// 简短日志头(不包含敏感信息)
console.log(`[BFF] ${ts} | forwarding to ${functionUrl.pathname}`);
let response;
try {
const ok = response.ok;
const status = response.status;
console.log(`[BFF] ${request.method} ${url.pathname} -> ${functionUrl.pathname} ${status}${ok ? '' : ' (fail)'}`);
} catch (_) {}
response = await fetch(proxiedRequest);
} catch (fetchErr) {
console.error(`[BFF] ${ts} | fetch to Function FAILED: ${fetchErr.message}`);
return new Response(JSON.stringify({ error: 'Bad Gateway', message: 'Failed to reach upstream function' }), {
status: 502,
headers: { 'Content-Type': 'application/json' }
});
}
// 响应日志(不包含敏感信息)
console.log(`[BFF] ${ts} | response from ${functionUrl.pathname}: status=${response.status} ok=${response.ok}`);
// 直接透传响应
return response;

View File

@@ -21,6 +21,9 @@ const graphqlSchema = {
};
exports.handler = withAuth(async (event, context, { auth, body }) => {
const ts = new Date().toISOString();
console.log(`[GGQL] ${ts} | invoked, operationName=${body.operationName || 'none'}, hasAccessToken=${!!body.accessToken}, hasMfaSignature=${!!body.mfaSignature}, hasCookie=${!!body.cookie}`);
// 解析请求体
const { mfaSignature, mfaRef, query, variables, operationName, cookie } = body;
@@ -125,6 +128,7 @@ exports.handler = withAuth(async (event, context, { auth, body }) => {
const verifyCookieUrl = hostHdr ? `${protoHdr}://${hostHdr}/.netlify/functions/verify-cookie` : ((process.env.URL || '').replace(/\/$/, '') + '/.netlify/functions/verify-cookie');
// 调用Giffgaff GraphQL API
console.log(`[GGQL] ${ts} | calling upstream: op=${opName}, isSwap=${isSwap}, hasMfaSignature=${!!mfaSignature}, hasResolvedMfaRef=${!!resolvedMfaRef}`);
let response;
try {
response = await axios.post(
@@ -132,13 +136,16 @@ exports.handler = withAuth(async (event, context, { auth, body }) => {
graphqlBody,
{ headers: requestHeaders, timeout: 30000 }
);
console.log(`[GGQL] ${ts} | upstream OK: status=${response.status}, hasData=${!!response.data}, hasErrors=${!!response.data?.errors}`);
} catch (err) {
const status = err.response?.status;
const data = err.response?.data || {};
const isUnauthorized = status === 401 || data?.error === 'unauthorized' || /invalid_token/i.test(String(data?.error || ''));
console.error(`[GGQL] ${ts} | upstream FAILED: status=${status}, isUnauthorized=${isUnauthorized}, errMsg=${err.message}`);
// 失败 401 时尝试用 cookie 刷新后重试一次
if (isUnauthorized && cookie) {
console.log(`[GGQL] ${ts} | attempting cookie-based token refresh`);
try {
const r = await axios.post(verifyCookieUrl, { cookie }, {
headers: { 'Content-Type': 'application/json' },
@@ -148,15 +155,19 @@ exports.handler = withAuth(async (event, context, { auth, body }) => {
if (r.data?.success && r.data?.accessToken) {
accessToken = r.data.accessToken;
requestHeaders['Authorization'] = `Bearer ${accessToken}`;
console.log(`[GGQL] ${ts} | token refreshed, retrying upstream call`);
response = await axios.post(
'https://publicapi.giffgaff.com/gateway/graphql',
graphqlBody,
{ headers: requestHeaders, timeout: 30000 }
);
console.log(`[GGQL] ${ts} | retry OK: status=${response.status}`);
} else {
console.error(`[GGQL] ${ts} | cookie refresh failed: success=${r.data?.success}`);
throw err;
}
} catch (reErr) {
console.error(`[GGQL] ${ts} | token refresh/retry FAILED: ${reErr.message}`);
throw new AuthError('Access token expired. Please re-login with cookie.', 401);
}
} else {

View File

@@ -92,23 +92,40 @@ class Logger {
}
/**
* 输出环境信息(启动时调用一次,仅开发环境
* 输出环境信息(启动时调用一次)
* 生产环境:仅输出关键排错信息(不含敏感数据)
* 开发环境:输出完整信息
*/
static env() {
if (!isDev || typeof window === 'undefined') return;
console.groupCollapsed('%c[ENV] 环境信息', 'color: #6366f1; font-weight: bold');
console.log('时间:', new Date().toISOString());
console.log('主机:', window.location.hostname);
console.log('协议:', window.location.protocol);
console.log('语言:', navigator.language);
console.log('平台:', navigator.platform);
console.log('UserAgent:', navigator.userAgent);
console.log('屏幕:', `${screen.width}x${screen.height}`);
console.log('视口:', `${window.innerWidth}x${window.innerHeight}`);
console.log('Cookie:', navigator.cookieEnabled ? '启用' : '禁用');
console.log('ServiceWorker:', 'serviceWorker' in navigator ? '支持' : '不支持');
console.log('NODE_ENV:', typeof process !== 'undefined' ? process.env?.NODE_ENV : 'N/A');
console.groupEnd();
if (typeof window === 'undefined') return;
if (isDev) {
// 开发环境:完整信息
console.groupCollapsed('%c[ENV] 环境信息', 'color: #6366f1; font-weight: bold');
console.log('时间:', new Date().toISOString());
console.log('主机:', window.location.hostname);
console.log('协议:', window.location.protocol);
console.log('语言:', navigator.language);
console.log('平台:', navigator.platform);
console.log('UserAgent:', navigator.userAgent);
console.log('屏幕:', `${screen.width}x${screen.height}`);
console.log('视口:', `${window.innerWidth}x${window.innerHeight}`);
console.log('Cookie:', navigator.cookieEnabled ? '启用' : '禁用');
console.log('ServiceWorker:', 'serviceWorker' in navigator ? '支持' : '不支持');
console.log('NODE_ENV:', typeof process !== 'undefined' ? process.env?.NODE_ENV : 'N/A');
console.groupEnd();
} else {
// 生产环境:关键排错信息(折叠分组,不占空间)
console.groupCollapsed('%c[ENV] 运行环境', 'color: #6b7280; font-size: 11px');
console.log('主机:', window.location.hostname);
console.log('协议:', window.location.protocol);
console.log('平台:', navigator.platform);
console.log('语言:', navigator.language);
console.log('Cookie:', navigator.cookieEnabled ? '启用' : '禁用');
console.log('ServiceWorker:', 'serviceWorker' in navigator ? '支持' : '不支持');
console.log('视口:', `${window.innerWidth}x${window.innerHeight}`);
console.groupEnd();
}
}
}