From c8afb3ca94da1e117d859e74cddb6bdff5d1f5dc Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Tue, 31 Jan 2023 02:21:19 -0300 Subject: [PATCH 1/3] :construction: context catch --- client/instanceProxyHandler.js | 20 ++++++++++++++--- server/printError.js | 3 +++ server/server.js | 5 +++++ tests/client.js | 4 ++++ tests/server.js | 8 +++++++ tests/src/Application.njs | 2 ++ tests/src/CatchError.njs | 39 ++++++++++++++++++++++++++++++++++ 7 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 tests/src/CatchError.njs diff --git a/client/instanceProxyHandler.js b/client/instanceProxyHandler.js index 6c68ac68..f8f16647 100644 --- a/client/instanceProxyHandler.js +++ b/client/instanceProxyHandler.js @@ -1,5 +1,5 @@ import client from './client' -import { generateContext } from './context' +import context, { generateContext } from './context' import { generateObjectProxy } from './objectProxyHandler' export const instanceProxies = new WeakMap() @@ -15,8 +15,22 @@ const instanceProxyHandler = { } const { [name]: named } = { [name]: (args) => { - const context = generateContext({ ...target._attributes, ...args }) - return target[name].call(proxy, context) + const scopedContext = generateContext({ ...target._attributes, ...args }) + let result + try { + result = target[name].call(proxy, scopedContext) + } catch (error) { + context.catch && context.catch(error) + return null + } + if (result instanceof Promise) { + return new Promise((resolve, reject) => { + result.then(resolve).catch((error) => { + context.catch ? context.catch(error) : reject(error) + }) + }) + } + return result }, } return named diff --git a/server/printError.js b/server/printError.js index a2daa484..c375cf76 100644 --- a/server/printError.js +++ b/server/printError.js @@ -1,4 +1,7 @@ +import context from './context' + export default function (error) { + context.catch && context.catch(error) const lines = error.stack.split(`\n`) let initiator = lines.find((line) => line.indexOf('Proxy') > -1) if (initiator) { diff --git a/server/server.js b/server/server.js index dac39bb9..30d023d6 100644 --- a/server/server.js +++ b/server/server.js @@ -270,6 +270,11 @@ server.start = function () { } }) + server.use((error, _request, response, _next) => { + printError(error) + response.status(500).json({}) + }) + if (!server.less) { if (!server.port) { console.info('\x1b[31mServer port is not defined!\x1b[0m') diff --git a/tests/client.js b/tests/client.js index 24b58b0e..48367738 100644 --- a/tests/client.js +++ b/tests/client.js @@ -12,4 +12,8 @@ context.start = function () { setTimeout(() => (context.startTimedValue = true), 1000) } +context.catch = async function (error) { + console.info(error.name) +} + export default context diff --git a/tests/server.js b/tests/server.js index 62df2bbb..953ae2ea 100644 --- a/tests/server.js +++ b/tests/server.js @@ -57,6 +57,10 @@ for (const method of methods) { }) } +context.server.get('/vaidamerda', (req, res) => { + res.vaidamerda() +}) + context.startIncrementalValue = 0 context.start = async function () { @@ -68,4 +72,8 @@ context.start = async function () { context.startIncrementalValue++ } +context.catch = async function (error) { + console.info(error.name) +} + export default context diff --git a/tests/src/Application.njs b/tests/src/Application.njs index 25ff8241..d96a64a1 100644 --- a/tests/src/Application.njs +++ b/tests/src/Application.njs @@ -4,6 +4,7 @@ import AnchorModifiers from './AnchorModifiers' import './Application.css' import ArrayAttributes from './ArrayAttributes' import BodyFragment from './BodyFragment' +import CatchError from './CatchError' import ChildComponent from './ChildComponent' import ComponentTernary from './ComponentTernary' import Context from './Context' @@ -137,6 +138,7 @@ class Application extends Nullstack { + ) diff --git a/tests/src/CatchError.njs b/tests/src/CatchError.njs new file mode 100644 index 00000000..2d2fc0e4 --- /dev/null +++ b/tests/src/CatchError.njs @@ -0,0 +1,39 @@ +import Nullstack from 'nullstack' + +const error = new Error('Panic in the system, someone missconfigured me!') + +class CatchError extends Nullstack { + + static async getServerFunction() { + throw error + } + + async oneErrorPls() { + throw error + } + + async hydrate({ params }) { + if (params.error === 'hydrate') { + this.vaidamerda() + } + } + + async initiate({ params, page }) { + if (params.error === 'initiate' && page.status === 200) { + this.vaidamerda() + } else if (params.error === 'initiateServerFunction') { + await this.getServerFunction() + } + } + + render() { + return ( +
+ +
+ ) + } + +} + +export default CatchError From 64aff90d36a090593a774ef7573040dc8c3dda5c Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Tue, 31 Jan 2023 15:51:22 -0300 Subject: [PATCH 2/3] :white_check_mark: test exceptions --- tests/client.js | 3 ++- tests/server.js | 7 ++++--- tests/src/CatchError.njs | 17 ++++++++++++++--- tests/src/CatchError.test.js | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 tests/src/CatchError.test.js diff --git a/tests/client.js b/tests/client.js index 48367738..4b7a6f8a 100644 --- a/tests/client.js +++ b/tests/client.js @@ -1,6 +1,7 @@ import Nullstack from 'nullstack' import Application from './src/Application' +import CatchError from './src/CatchError.njs' import vueable from './src/plugins/vueable' Nullstack.use(vueable) @@ -13,7 +14,7 @@ context.start = function () { } context.catch = async function (error) { - console.info(error.name) + CatchError.logError({ message: error.message }) } export default context diff --git a/tests/server.js b/tests/server.js index 953ae2ea..4894443b 100644 --- a/tests/server.js +++ b/tests/server.js @@ -3,6 +3,7 @@ import Nullstack from 'nullstack' import cors from 'cors' import Application from './src/Application' +import CatchError from './src/CatchError' import ContextProject from './src/ContextProject' import ContextSecrets from './src/ContextSecrets' import ContextSettings from './src/ContextSettings' @@ -57,8 +58,8 @@ for (const method of methods) { }) } -context.server.get('/vaidamerda', (req, res) => { - res.vaidamerda() +context.server.get('/vaidamerdanaapi.json', (_request, response) => { + response.vaidamerdanaapi() }) context.startIncrementalValue = 0 @@ -73,7 +74,7 @@ context.start = async function () { } context.catch = async function (error) { - console.info(error.name) + CatchError.logError({ message: error.message }) } export default context diff --git a/tests/src/CatchError.njs b/tests/src/CatchError.njs index 2d2fc0e4..f7e5d6ec 100644 --- a/tests/src/CatchError.njs +++ b/tests/src/CatchError.njs @@ -1,9 +1,16 @@ import Nullstack from 'nullstack' +import { appendFileSync } from 'fs' + const error = new Error('Panic in the system, someone missconfigured me!') class CatchError extends Nullstack { + static async logError({ message, environment }) { + const folder = environment.production ? '.production' : '.development' + appendFileSync(`${folder}/exceptions.txt`, `${message}\n`) + } + static async getServerFunction() { throw error } @@ -14,13 +21,17 @@ class CatchError extends Nullstack { async hydrate({ params }) { if (params.error === 'hydrate') { - this.vaidamerda() + this.vaidamerdanoclient() } } + update() { + this.hydrated = true + } + async initiate({ params, page }) { if (params.error === 'initiate' && page.status === 200) { - this.vaidamerda() + this.vaidamerdanoserver() } else if (params.error === 'initiateServerFunction') { await this.getServerFunction() } @@ -28,7 +39,7 @@ class CatchError extends Nullstack { render() { return ( -
+
) diff --git a/tests/src/CatchError.test.js b/tests/src/CatchError.test.js new file mode 100644 index 00000000..838aff2c --- /dev/null +++ b/tests/src/CatchError.test.js @@ -0,0 +1,34 @@ +const { readFileSync } = require('fs') + +describe('CatchError initiate', () => { + test('errors caused by instances can be caught on the server', async () => { + await page.goto('http://localhost:6969/catch-error?error=initiate') + const logs = readFileSync('.production/exceptions.txt', 'utf-8') + expect(logs.includes('this.vaidamerdanoserver is not a function')).toBeTruthy() + }) +}) + +describe('CatchError initiateServerFunction', () => { + test('errors caused by server functions can be caught on the server', async () => { + await page.goto('http://localhost:6969/catch-error?error=initiateServerFunction') + const logs = readFileSync('.production/exceptions.txt', 'utf-8') + expect(logs.includes('Panic in the system, someone missconfigured me!')).toBeTruthy() + }) +}) + +describe('CatchError hydrate', () => { + test('errors caused by instances can be caught on the client', async () => { + await page.goto('http://localhost:6969/catch-error?error=hydrate') + await page.waitForSelector('[data-hydrated]') + const logs = readFileSync('.production/exceptions.txt', 'utf-8') + expect(logs.includes('this.vaidamerdanoclient is not a function')).toBeTruthy() + }) +}) + +describe('CatchError api', () => { + test('errors caused by api routes can be caught on the server', async () => { + await page.goto('http://localhost:6969/vaidamerdanaapi.json') + const logs = readFileSync('.production/exceptions.txt', 'utf-8') + expect(logs.includes('response.vaidamerdanaapi is not a function')).toBeTruthy() + }) +}) From befd7cef501552f2e985183077e07211f7050546 Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Tue, 31 Jan 2023 20:52:37 -0300 Subject: [PATCH 3/3] :bug: consistent reqres --- server/reqres.js | 20 +++++++++- server/server.js | 16 +++++--- tests/client.js | 1 + tests/server.js | 5 +++ tests/src/Application.njs | 2 + tests/src/ReqRes.njs | 82 +++++++++++++++++++++++++++++++++++++++ tests/src/ReqRes.test.js | 42 ++++++++++++++++++++ 7 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 tests/src/ReqRes.njs create mode 100644 tests/src/ReqRes.test.js diff --git a/server/reqres.js b/server/reqres.js index c743ad43..13cdb10c 100644 --- a/server/reqres.js +++ b/server/reqres.js @@ -1,2 +1,20 @@ -const reqres = {} +class ReqRes { + + request = null + response = null + + set(request, response) { + this.request = request + this.response = response + } + + clear() { + this.request = null + this.response = null + } + +} + +const reqres = new ReqRes() + export default reqres diff --git a/server/server.js b/server/server.js index 30d023d6..96766186 100644 --- a/server/server.js +++ b/server/server.js @@ -40,6 +40,7 @@ for (const method of ['get', 'post', 'put', 'patch', 'delete', 'all']) { server[method] = function (...args) { if (typeof args[1] === 'function' && args[1].name === '_invoke') { return original(args[0], bodyParser.text({ limit: server.maximumPayloadSize }), async (request, response) => { + reqres.set(request, response) const params = {} for (const key of Object.keys(request.params)) { params[key] = extractParamValue(request.params[key]) @@ -53,9 +54,11 @@ for (const method of ['get', 'post', 'put', 'patch', 'delete', 'all']) { try { const subcontext = generateContext({ request, response, ...params }) const result = await args[1](subcontext) + reqres.clear() response.json(result) } catch (error) { printError(error) + reqres.clear() response.status(500).json({}) } }) @@ -224,6 +227,7 @@ server.start = function () { server.all(`/${prefix}/:hash/:methodName.json`, async (request, response) => { const payload = request.method === 'GET' ? request.query.payload : request.body + reqres.set(request, response) const args = deserialize(payload) const { hash, methodName } = request.params const [invokerHash, boundHash] = hash.split('-') @@ -241,12 +245,15 @@ server.start = function () { try { const subcontext = generateContext({ request, response, ...args }) const result = await method.call(boundKlass, subcontext) + reqres.clear() response.json({ result }) } catch (error) { printError(error) + reqres.clear() response.status(500).json({}) } } else { + reqres.clear() response.status(404).json({}) } }) @@ -255,18 +262,15 @@ server.start = function () { if (request.originalUrl.split('?')[0].indexOf('.') > -1) { return next() } - reqres.request = request - reqres.response = response + reqres.set(request, response) const scope = await prerender(request, response) if (!response.headersSent) { const status = scope.context.page.status const html = template(scope) - reqres.request = null - reqres.response = null + reqres.clear() response.status(status).send(html) } else { - reqres.request = null - reqres.response = null + reqres.clear() } }) diff --git a/tests/client.js b/tests/client.js index 4b7a6f8a..5fe6e42f 100644 --- a/tests/client.js +++ b/tests/client.js @@ -15,6 +15,7 @@ context.start = function () { context.catch = async function (error) { CatchError.logError({ message: error.message }) + console.error(error) } export default context diff --git a/tests/server.js b/tests/server.js index 4894443b..715265c4 100644 --- a/tests/server.js +++ b/tests/server.js @@ -10,6 +10,7 @@ import ContextSettings from './src/ContextSettings' import ContextWorker from './src/ContextWorker' import ExposedServerFunctions from './src/ExposedServerFunctions' import vueable from './src/plugins/vueable' +import ReqRes from './src/ReqRes' Nullstack.use(vueable) @@ -41,6 +42,9 @@ context.server.put('/data/put/:param', ExposedServerFunctions.getData) context.server.patch('/data/patch/:param', ExposedServerFunctions.getData) context.server.delete('/data/delete/:param', ExposedServerFunctions.getData) +context.server.get('/exposed-server-function-url.json', ReqRes.exposedServerFunction) +context.server.get('/nested-exposed-server-function-url.json', ReqRes.nestedExposedServerFunction) + context.server.get('/custom-api-before-start', (request, response) => { response.json({ startValue: context.startValue }) }) @@ -75,6 +79,7 @@ context.start = async function () { context.catch = async function (error) { CatchError.logError({ message: error.message }) + console.error(error) } export default context diff --git a/tests/src/Application.njs b/tests/src/Application.njs index d96a64a1..997742df 100644 --- a/tests/src/Application.njs +++ b/tests/src/Application.njs @@ -41,6 +41,7 @@ import PublicServerFunctions from './PublicServerFunctions.njs' import PureComponents from './PureComponents' import Refs from './Refs' import RenderableComponent from './RenderableComponent' +import ReqRes from './ReqRes' import RoutesAndParams from './RoutesAndParams' import RouteScroll from './RouteScroll' import ServerFunctions from './ServerFunctions' @@ -139,6 +140,7 @@ class Application extends Nullstack { + ) diff --git a/tests/src/ReqRes.njs b/tests/src/ReqRes.njs new file mode 100644 index 00000000..8e10323f --- /dev/null +++ b/tests/src/ReqRes.njs @@ -0,0 +1,82 @@ +import Nullstack from 'nullstack' + +class ReqRes extends Nullstack { + + static async innerNestedServerFunctionFromClient(context) { + return context.request.originalUrl + } + + static async nestedServerFunctionFromClient() { + return this.innerNestedServerFunctionFromClient() + } + + static async serverFunctionFromClient(context) { + return context.request.originalUrl + } + + static async innerNestedServerFunction(context) { + return context.request.originalUrl + } + + static async serverFunction(context) { + return context.request.originalUrl + } + + static async nestedServerFunction() { + return this.innerNestedServerFunction() + } + + static async innerNestedExposedServerFunction(context) { + return context.request.originalUrl + } + + static async nestedExposedServerFunction() { + return this.innerNestedExposedServerFunction() + } + + static async exposedServerFunction(context) { + return context.request.originalUrl + } + + async initiate() { + this.serverFunctionUrl = await ReqRes.serverFunction() + this.nestedServerFunctionUrl = await ReqRes.nestedServerFunction() + } + + async fetchServerFunction({ slug }) { + const response = await fetch(`/${slug}.json`) + return response.json() + } + + async hydrate() { + this.serverFunctionFromClientUrl = await ReqRes.serverFunctionFromClient() + this.nestedServerFunctionFromClientUrl = await ReqRes.nestedServerFunctionFromClient() + this.exposedServerFunctionUrl = await this.fetchServerFunction({ slug: 'exposed-server-function-url' }) + this.nestedExposedServerFunctionUrl = await this.fetchServerFunction({ slug: 'nested-exposed-server-function-url' }) + } + + render() { + return ( +
+ ) + } + +} + +export default ReqRes diff --git a/tests/src/ReqRes.test.js b/tests/src/ReqRes.test.js new file mode 100644 index 00000000..6c52e245 --- /dev/null +++ b/tests/src/ReqRes.test.js @@ -0,0 +1,42 @@ +beforeAll(async () => { + await page.goto('http://localhost:6969/reqres') + await page.waitForSelector('[data-hydrated]') +}) + +describe('ReqRes', () => { + test('server functions keep the request', async () => { + await page.waitForSelector('[data-server-function]') + const element = await page.$('[data-server-function]') + expect(element).toBeTruthy() + }) + + test('nested server functions keep the request', async () => { + await page.waitForSelector('[data-nested-server-function]') + const element = await page.$('[data-nested-server-function]') + expect(element).toBeTruthy() + }) + + test('server functions keep the request when invoked by the client', async () => { + await page.waitForSelector('[data-server-function-from-client]') + const element = await page.$('[data-server-function-from-client]') + expect(element).toBeTruthy() + }) + + test('nested server functions keep the request when invoked by the client', async () => { + await page.waitForSelector('[data-nested-server-function-from-client]') + const element = await page.$('[data-nested-server-function-from-client]') + expect(element).toBeTruthy() + }) + + test('exposed server functions keep the request', async () => { + await page.waitForSelector('[data-exposed-server-function]') + const element = await page.$('[data-exposed-server-function]') + expect(element).toBeTruthy() + }) + + test('nested exposed server functions keep the request', async () => { + await page.waitForSelector('[data-exposed-nested-server-function]') + const element = await page.$('[data-exposed-nested-server-function]') + expect(element).toBeTruthy() + }) +})