Lambda@Edge返回200但页面样式无法加载问题求助
问题分析与解决办法
核心问题排查
页面返回200但空白且无报错,结合代码逻辑,主要存在以下关键问题:
1. Content-Type 头部未动态更新
代码中staticHeaders的content-type值初始化后固定为text/html,处理JS/CSS文件时仅修改了contentType变量,但未同步更新staticHeaders内的对应值。浏览器收到JS/CSS文件时,头部仍标记为text/html,无法正确解析资源,导致页面依赖的脚本、样式失效,最终显示空白。
2. 异步处理逻辑错误(根路径HTML替换)
根路径处理时,代码错误地将replaceAll直接调用在Promise对象上:
contents = await fetchIndex('index.html').replaceAll("<script ", `<script nonce="${nonce}" `);
await仅作用于fetchIndex,但.replaceAll是直接在未解析的Promise对象上执行,导致contents变成无效的Promise对象,返回给浏览器的内容异常,页面空白。
3. 非根路径资源缺少Content-Type头部
最后一个else分支中,返回的响应头仅包含CSP,未设置content-type,浏览器无法识别资源类型,导致图片、其他页面资源无法正确渲染。
4. Async函数与Callback混用
Handler声明为async函数,但同时使用callback返回响应,可能导致Lambda@Edge的响应处理时序混乱,引发隐性异常。
代码修复方案
针对以上问题,修改后的完整代码如下:
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { randomBytes } from "crypto"; const region = "eu-west-2"; const s3Client = new S3Client({ region }); const bucketName = "mywebsitebucket"; // 生成通用CSP规则 const getGeneralCsp = () => "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; connect-src 'self' data: https://api.mywebsite https://cognito-idp.eu-west-2.amazonaws.com/ https://api.gleif.org; font-src 'self'; frame-src 'self'; img-src data: 'self'; manifest-src 'self'; media-src 'self'; form-action 'none'; frame-ancestors 'self';"; // 生成带Nonce的CSP规则 const generateCsp = () => { const nonce = randomBytes(16).toString("base64"); return { csp: `default-src 'self'; script-src 'nonce-${nonce}' 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; connect-src 'self' data: https://api.mywebsite https://cognito-idp.eu-west-2.amazonaws.com/ https://api.gleif.org; font-src 'self'; frame-src 'self'; img-src data: 'self'; manifest-src 'self'; media-src 'self'; form-action 'none'; frame-ancestors 'self';`, nonce, }; }; // 从S3获取文件内容并转为字符串 async function fetchIndex(key) { try{ console.log(`Key is -------------${key}`); const command = new GetObjectCommand({ Bucket: bucketName, Key: key }); const response = await s3Client.send(command); const streamToString = (stream) => new Promise((resolve, reject) => { const chunks = []; stream.on('data', (chunk) => chunks.push(chunk)); stream.on('error', reject); stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); }); return await streamToString(response.Body); } catch (err) { console.error('Error fetching file from S3:', err); throw err; } } export const handler = async (event) => { const request = event.Records[0].cf.request; const uri = request.uri; const modifiedUri = uri.startsWith("/") ? uri.substring(1) : uri; const {csp, nonce} = generateCsp(); const generalCsp = getGeneralCsp(); try { // 按资源类型分情况处理 if (uri.endsWith('.js')) { const contents = await fetchIndex(modifiedUri); return { status: '200', body: contents, headers: { "content-type": [{key: "Content-Type", value: "application/javascript"}], "content-security-policy": [{key: "Content-Security-Policy", value: generalCsp}] } }; } else if (uri.endsWith('.css')) { const contents = await fetchIndex(modifiedUri); return { status: '200', body: contents, headers: { "content-type": [{key: "Content-Type", value: "text/css"}], "content-security-policy": [{key: "Content-Security-Policy", value: generalCsp}] } }; } else if(uri === "/") { const contents = await fetchIndex('index.html'); // 先获取HTML内容再执行nonce替换 const modifiedContents = contents.replaceAll("<script ", `<script nonce="${nonce}" `); return { status: '200', body: modifiedContents, headers: { "content-type": [{key: "Content-Type", value: "text/html"}], "content-security-policy": [{key: "Content-Security-Policy", value: csp}] } }; } else { const contents = await fetchIndex(modifiedUri); // 根据后缀自动匹配Content-Type,可按需扩展 let contentType = 'text/html'; if(uri.endsWith('.png')) contentType = 'image/png'; if(uri.endsWith('.jpg') || uri.endsWith('.jpeg')) contentType = 'image/jpeg'; if(uri.endsWith('.svg')) contentType = 'image/svg+xml'; return { status: '200', body: contents, headers: { "content-type": [{key: "Content-Type", value: contentType}], "content-security-policy": [{key: "Content-Security-Policy", value: generalCsp}] } }; } } catch (error) { // 统一错误响应处理 let errorMsg = 'Not Found'; if(uri.endsWith('.js')) errorMsg = 'Not Found JS'; else if(uri.endsWith('.css')) errorMsg = 'Not Found CSS'; else if(uri === "/") errorMsg = 'Not Found Index'; return { status: '404', body: errorMsg, headers: { "content-type": [{key: "Content-Type", value: "text/plain"}], "content-security-policy": [{key: "Content-Security-Policy", value: generalCsp}] } }; } };
验证步骤
- 部署修改后的Lambda@Edge函数到CloudFront,确保关联的是viewer request事件。
- 打开浏览器开发者工具的网络面板,检查每个资源的
Content-Type头部是否与资源类型匹配。 - 查看控制台面板,确认是否存在脚本加载、解析类错误(修复后若有其他问题会显性展示)。
- 手动清除CloudFront缓存,避免旧的错误响应干扰测试结果。
内容的提问来源于stack exchange,提问作者ShanWave007
相关产品推荐
相关产品推荐

