diff --git a/src/lib/functions/synchronous.js b/src/lib/functions/synchronous.js index e203fbf3a2c..2af21d4d5af 100644 --- a/src/lib/functions/synchronous.js +++ b/src/lib/functions/synchronous.js @@ -1,9 +1,8 @@ // @ts-check const { Buffer } = require('buffer') -const { readFile } = require('fs').promises -const { join } = require('path') const { NETLIFYDEVERR } = require('../../utils') +const renderErrorTemplate = require('../render-error-remplate') const { detectAwsSdkError } = require('./utils') @@ -47,25 +46,13 @@ const formatLambdaLocalError = (err, acceptsHtml) => }) : `${err.errorType}: ${err.errorMessage}\n ${err.stackTrace.join('\n ')}` -let errorTemplateFile - -const renderErrorTemplate = async (errString) => { - const regexPattern = //g - const templatePath = './templates/function-error.html' - - try { - errorTemplateFile = errorTemplateFile || (await readFile(join(__dirname, templatePath), 'utf-8')) - return errorTemplateFile.replace(regexPattern, errString) - } catch { - return errString - } -} - const processRenderedResponse = async (err, request) => { const acceptsHtml = request.headers && request.headers.accept && request.headers.accept.includes('text/html') const errorString = typeof err === 'string' ? err : formatLambdaLocalError(err, acceptsHtml) - return acceptsHtml ? await renderErrorTemplate(errorString) : errorString + return acceptsHtml + ? await renderErrorTemplate(errorString, './templates/function-error.html', 'function') + : errorString } const handleErr = async (err, request, response) => { diff --git a/src/lib/render-error-remplate.js b/src/lib/render-error-remplate.js new file mode 100644 index 00000000000..766f153b666 --- /dev/null +++ b/src/lib/render-error-remplate.js @@ -0,0 +1,17 @@ +const { readFile } = require('fs').promises +const { join } = require('path') + +let errorTemplateFile + +const renderErrorTemplate = async (errString, templatePath, functionType) => { + const errorDetailsRegex = //g + const functionTypeRegex = //g + try { + errorTemplateFile = errorTemplateFile || (await readFile(join(__dirname, templatePath), 'utf-8')) + return errorTemplateFile.replace(errorDetailsRegex, errString).replace(functionTypeRegex, functionType) + } catch { + return errString + } +} + +module.exports = renderErrorTemplate diff --git a/src/lib/functions/templates/function-error.html b/src/lib/templates/function-error.html similarity index 81% rename from src/lib/functions/templates/function-error.html rename to src/lib/templates/function-error.html index 480dba0c4ec..b32ee4231b1 100644 --- a/src/lib/functions/templates/function-error.html +++ b/src/lib/templates/function-error.html @@ -24,6 +24,7 @@ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; color: var(--colorDefaultTextColor); + margin: 0; } main { @@ -196,7 +197,9 @@

fill="#900B31" /> - This function has crashed + This + + has crashed

An unhandled error in the function code triggered the following message:

Stack trace

- - - -
-

Next steps

- -
- - diff --git a/src/utils/proxy.js b/src/utils/proxy.js index c39be9695e1..fa776188a94 100644 --- a/src/utils/proxy.js +++ b/src/utils/proxy.js @@ -6,6 +6,8 @@ const http = require('http') const https = require('https') const { isIPv6 } = require('net') const path = require('path') +const util = require('util') +const zlib = require('zlib') const contentType = require('content-type') const cookie = require('cookie') @@ -21,14 +23,32 @@ const toReadableStream = require('to-readable-stream') const edgeFunctions = require('../lib/edge-functions') const { fileExistsAsync, isFileAsync } = require('../lib/fs') +const renderErrorTemplate = require('../lib/render-error-remplate') const { NETLIFYDEVLOG, NETLIFYDEVWARN } = require('./command-helpers') const { createStreamPromise } = require('./create-stream-promise') const { headersForPath, parseHeaders } = require('./headers') const { createRewriter, onChanges } = require('./rules-proxy') +const decompress = util.promisify(zlib.gunzip) const shouldGenerateETag = Symbol('Internal: response should generate ETag') +const formatEdgeFunctionError = (errorBuffer, acceptsHtml) => { + const { + error: { message, name, stack }, + } = JSON.parse(errorBuffer.toString()) + + if (!acceptsHtml) { + return `${name}: ${message}\n ${stack}` + } + + return JSON.stringify({ + errorType: name, + errorMessage: message, + trace: stack.split('\\n'), + }) +} + const isInternal = function (url) { return url.startsWith('/.netlify/') } @@ -363,7 +383,7 @@ const initializeProxy = async function ({ configPath, distDir, host, port, proje responseData.push(data) }) - proxyRes.on('end', function onEnd() { + proxyRes.on('end', async function onEnd() { const responseBody = Buffer.concat(responseData) let responseStatus = req.proxyOptions.status || proxyRes.statusCode @@ -387,6 +407,22 @@ const initializeProxy = async function ({ configPath, distDir, host, port, proje res.setHeader(key, val) }) + const isUncaughtError = proxyRes.headers['x-nf-uncaught-error'] === '1' + + if (edgeFunctions.isEdgeFunctionsRequest(req) && isUncaughtError) { + const acceptsHtml = req.headers && req.headers.accept && req.headers.accept.includes('text/html') + const decompressedBody = await decompress(responseBody) + const formattedBody = formatEdgeFunctionError(decompressedBody, acceptsHtml) + const errorResponse = acceptsHtml + ? await renderErrorTemplate(formattedBody, './templates/function-error.html', 'edge function') + : formattedBody + const contentLength = Buffer.from(errorResponse, 'utf8').byteLength + + res.setHeader('content-length', contentLength) + res.write(errorResponse) + return res.end() + } + res.writeHead(responseStatus, proxyRes.headers) if (responseStatus !== 304) {