Skip to content

Commit

Permalink
Feat: add error page to edge functions for local dev (#5070)
Browse files Browse the repository at this point in the history
* feat: getting error page to edge functions

* feat: set content length to have correct headers

* feat: move render-error-template to own file

* feat: render edge function error in its own template

* feat: parse error in proxy file

* feat: change parsing in formatEdgeFunctionError, remove default fallback in renderer

* feat: remove check on 500 status code

* feat: remove redundant function error template and clean old one
  • Loading branch information
khendrikse committed Oct 3, 2022
1 parent 91506bb commit b9e0917
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 70 deletions.
21 changes: 4 additions & 17 deletions 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')

Expand Down Expand Up @@ -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 = /<!--@ERROR-DETAILS-->/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) => {
Expand Down
17 changes: 17 additions & 0 deletions 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 = /<!--@ERROR-DETAILS-->/g
const functionTypeRegex = /<!--@FUNCTION-TYPE-->/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
Expand Up @@ -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 {
Expand Down Expand Up @@ -196,7 +197,9 @@ <h1>
fill="#900B31"
/>
</svg>
This function has crashed
This
<!--@FUNCTION-TYPE-->
has crashed
</h1>
<p>An unhandled error in the function code triggered the following message:</p>
<p class="error-message-details hidden inline-code">
Expand All @@ -210,34 +213,7 @@ <h1>
<h3>Stack trace</h3>
<pre><code></code></pre>
</div>

<div class="request-id-container hidden">
<h3 class="connection-details">Connection details</h3>
<p>
Netlify internal ID: <span class="inline-code request-id"><!--@REQUEST-ID--></span>
</p>
</div>
</section>

<section class="next-steps">
<h1>Next steps</h1>
<ul>
<li>Site visitors: Contact the site owner or try again.</li>
<li>
Site owners: Review the
<a class="fn-logs-link" href="https://docs.netlify.com/functions/logs/#access-function-logs" target="_blank"
>function logs</a
>
for more info.
</li>
</ul>
</section>

<div class="hidden">
<span class="site-id"><!--@SITE-ID--></span>
<span class="deploy-id"><!--@DEPLOY-ID--></span>
<span class="fn-name"><!--@FUNCTION-NAME--></span>
</div>
<script>
/* eslint eslint-comments/no-use: off */
// eslint-disable-next-line func-names
Expand All @@ -248,17 +224,6 @@ <h1>Next steps</h1>
const errorMessageElement = document.querySelector('.error-message')
const stackTraceElement = document.querySelector('.stack-trace')
const stackTraceCodeElement = document.querySelector('.stack-trace code')
const requestIdContainerElement = document.querySelector('.request-id-container')
const requestIdElement = document.querySelector('.request-id')
const siteIdVal = document.querySelector('.site-id').textContent.trim()
const deployIdVal = document.querySelector('.deploy-id').textContent.trim()
const fnNameVal = document.querySelector('.fn-name').textContent.trim()
const logsLink = document.querySelector('.fn-logs-link')

// NO request ID Option Provided - only expected to happen in local dev scenarios
if (requestIdElement.textContent.trim().length !== 0) {
requestIdContainerElement.classList.remove('hidden')
}

// Enriching the error details
let parsedErrorDetails
Expand Down Expand Up @@ -339,19 +304,6 @@ <h1>Next steps</h1>
} else {
stackTraceCodeElement.remove()
}

// only create the dynamic link if we have all of the relevant details
// Add a defensive check for "undefined" accidentally being put into the template
if (
deployIdVal &&
deployIdVal !== 'undefined' &&
siteIdVal &&
siteIdVal !== 'undefined' &&
fnNameVal &&
siteIdVal !== 'undefined'
) {
logsLink.href = `https://app.netlify.com/site-fn-logs/${siteIdVal}/${deployIdVal}/${fnNameVal}`
}
})()
</script>
</main>
Expand Down
38 changes: 37 additions & 1 deletion src/utils/proxy.js
Expand Up @@ -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')
Expand All @@ -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/')
}
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down

1 comment on commit b9e0917

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📊 Benchmark results

Package size: 223 MB

Please sign in to comment.