Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions server/errorHandleServerFunctions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import context from './context'

/**
* Turns a thrown server-function exception into an HTTP JSON response.
*
* ## Throw contract
*
* Prefer throwing an `Error` instance (or subclass). Plain objects are tolerated
* so the request still completes, but they skip stack-based logging details.
*
* ```js
* throw new Error('unexpected failure')
*
* // tolerated (unusual) — must not crash the server
* throw { status: 200, result: { code: 'UNKNOWN_ERROR', message: 'Erro Interno' } }
*
* class BusinessError extends Error {
* constructor(message, { status = 422, code } = {}) {
* super(message)
* this.status = status
* this.code = code
* }
* toJSON() {
* return { message: this.message, code: this.code }
* }
* }
* throw new BusinessError('invalid input', { code: 'INVALID' })
* ```
*
* ## Customize via `context.onerror`
*
* ```js
* context.onerror = (error) => ({
* status: error.status || 500,
* result: {
* message: error.message,
* code: error.code,
* },
* })
* ```
*
* Return shape:
* - `{ status?, result? }` — preferred; `status` becomes the HTTP status, `result` the JSON body
* - any other non-null value — used as body; status falls back to `error.status || 500`
* - `null` / `undefined` — skip to the next resolution step
*
* Resolution order:
* 1. `context.onerror(error)` when defined
* 2. `error.toJSON()` when available (custom Error subclass)
* 3. empty `{}` with status `500` (default — no leak)
*
* @param {import('express').Response} response
* @param {Error & { status?: number, code?: string | number, toJSON?: () => unknown } | Record<string, unknown>} error
* @param {{wrapResult?: boolean}} [options]
* - wrapResult: true for invoker routes (`{ result }`), false for exposed `_invoke` routes
*/
export default function errorHandleServerFunction(
response,
error,
{ wrapResult = true } = {},
) {
let status = 500
let result = null

if (typeof context.onerror === 'function') {
const handled = context.onerror(error)
if (handled != null) {
if (
typeof handled === 'object' &&
('result' in handled || 'status' in handled)
) {
status = handled.status || error?.status || 500
result = 'result' in handled ? handled.result : handled
} else {
status = error?.status || 500
result = handled
}
} else if (error && typeof error.toJSON === 'function') {
result = error.toJSON()
status = error.status || 500
}
} else if (error && typeof error.toJSON === 'function') {
result = error.toJSON()
status = error.status || 500
}

if (result === null) {
return response.status(500).json({})
}

return response.status(status).json(wrapResult ? { result } : result)
}
3 changes: 2 additions & 1 deletion server/exposeServerFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import bodyParser from 'body-parser'
import path from 'path'
import deserialize from '../shared/deserialize'
import { getCurrentContext } from './context'
import errorHandleServerFunction from './errorHandleServerFunctions'
import printError from './printError'
import registry from './registry'

Expand Down Expand Up @@ -31,7 +32,7 @@ export default function exposeServerFunctions(server) {
response.json(result)
} catch (error) {
printError(error)
response.status(500).json({})
errorHandleServerFunction(response, error, { wrapResult: false })
}
})
}
Expand Down
16 changes: 13 additions & 3 deletions server/printError.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ import context from './context'

export default function (error) {
if (context.catch) {
context.catch(error)
try {
context.catch(error)
} catch (_) {
// user-land catch must not break error reporting
}
}
const lines = error.stack.split(`\n`)

const name = error?.name || 'Error'
const message =
error?.message ??
(error && typeof error === 'object' ? JSON.stringify(error) : String(error))
const lines = typeof error?.stack === 'string' ? error.stack.split(`\n`) : []

let initiator = lines.find((line) => line.indexOf('Proxy') > -1)
if (initiator) {
initiator = initiator.split('(')[0]
Expand Down Expand Up @@ -34,7 +44,7 @@ export default function (error) {
}
}
console.info()
console.info('\x1b[31m', error.name, '-', error.message, '\x1b[0m')
console.info('\x1b[31m', name, '-', message, '\x1b[0m')
console.info()
if (initiator) {
console.info('\x1b[2m', 'initiator:', '\x1b[0m', '\x1b[37m', initiator, '\x1b[0m')
Expand Down
7 changes: 4 additions & 3 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import generateRobots from './robots'
import template from './template'
import { generateServiceWorker } from './worker'
import { load } from './lazy'
import errorHandleServerFunction from './errorHandleServerFunctions'

const server = express()

Expand Down Expand Up @@ -143,7 +144,7 @@ server.start = function () {
response.json({ result })
} catch (error) {
printError(error)
response.status(500).json({})
errorHandleServerFunction(response, error)
}
} else {
response.status(404).json({})
Expand Down Expand Up @@ -180,7 +181,7 @@ server.start = function () {
response.json({ result })
} catch (error) {
printError(error)
response.status(500).json({})
errorHandleServerFunction(response, error)
}
} else {
response.status(404).json({})
Expand Down Expand Up @@ -214,7 +215,7 @@ server.start = function () {

server.use((error, _request, response, _next) => {
printError(error)
response.status(500).json({})
errorHandleServerFunction(response, error)
})

if (module.hot) {
Expand Down
32 changes: 32 additions & 0 deletions tests/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import ExposedServerFunctions from './src/ExposedServerFunctions'
import setExternalRoute from './src/externalRoute'
import vueable from './src/plugins/vueable'
import ReqRes from './src/ReqRes'
import AppBusinessException from './src/AppBusinessException'
import ErrorHandleServerFunctions from './src/ErrorHandleServerFunctions'

Nullstack.use(vueable)

Expand Down Expand Up @@ -69,6 +71,14 @@ context.server.get('/vaidamerdanaapi.json', (_request, response) => {
response.vaidamerdanaapi()
})

context.server.get(
'/error-handle/exposed-business.json',
ErrorHandleServerFunctions.throwAppBusinessError,
)
context.server.get('/error-handle/exposed-normal.json', ErrorHandleServerFunctions.throwNormalError)
context.server.get('/error-handle/exposed-json.json', ErrorHandleServerFunctions.throwJSONError)
context.server.get('/error-handle/exposed-dummy.json', ErrorHandleServerFunctions.throwDummyJsonError)

context.startIncrementalValue = 0

setExternalRoute(context.server)
Expand All @@ -94,4 +104,26 @@ context.catch = async function (error) {
}
}


context.onerror = function (error) {
//Dentro do result construimos da nossa forma
//result e status é obrigatorio para o padrao do nullstack
if(error instanceof AppBusinessException){
return {
status: 422,
result: {
message: `Business Exception: ${error.message}`,
code: error.code,
},
}
}
return {
status: error.status || 500,
result: {
message: error.message,
code: error.code || 'ERROR_UNKNOWN',
},
}
}

export default context
10 changes: 10 additions & 0 deletions tests/src/AppBusinessException.njs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default class AppBusinessException extends Error {
constructor(message, { status = 422, code } = {}) {
super(message)
this.status = status
this.code = code
}
toJSON() {
return { message: this.message, code: this.code }
}
}
2 changes: 2 additions & 0 deletions tests/src/Application.njs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ContextWorker from './ContextWorker'
import DateParser from './DateParser'
import DynamicHead from './DynamicHead'
import Element from './Element'
import ErrorHandleServerFunctions from './ErrorHandleServerFunctions'
import ErrorOnChildNode from './ErrorOnChildNode'
import ErrorPage from './ErrorPage'
import ExposedServerFunctions from './ExposedServerFunctions'
Expand Down Expand Up @@ -151,6 +152,7 @@ class Application extends Nullstack {
<IsomorphicImport route="/isomorphic-import" />
<ExposedServerFunctions route="/exposed-server-functions" />
<CatchError route="/catch-error" />
<ErrorHandleServerFunctions route="/error-handle-server-functions" />
<ReqRes route="/reqres" />
<Logo route="/logo" />
<NestedFolder route="/nested/folder" />
Expand Down
50 changes: 50 additions & 0 deletions tests/src/ErrorHandleServerFunctions.njs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Nullstack from 'nullstack'
import AppBusinessException from './AppBusinessException.njs'

class ErrorHandleServerFunctions extends Nullstack {

static async throwAppBusinessError() {
throw new AppBusinessException('Aconteceu um erro de regra de negocio', {
status: 422,
code: 'BUSINESS_VALIDATION',
})
}

static async throwNormalError() {
throw new Error('Aconteceu um problema nao esperado')
}

static async throwJSONError() {
// Esse é modificado no retorno devido ao context.onerror
throw { result: {code: 'UNKNOWN_ERROR', status: 500, message:'Erro Interno', teste:"2"}, status: 400}
}

static async throwDummyJsonError() {
throw { code: 'UNKNOWN_ERROR', status: 500, message:'Erro Interno' }
}

async run() {
const case1 = await this.throwAppBusinessError()
console.dir({ res: case1 }, { depth: null })

const case2 = await this.throwNormalError()
console.dir({ res: case2 }, { depth: null })

const case3 = await this.throwJSONError()
console.dir({ res: case3 }, { depth: null })

const case4 = await this.throwDummyJsonError()
console.dir({ res: case4 }, { depth: null })
}

render() {
return (
<div data-error-handle data-hydrated={this.hydrated}>
<button data-run onclick={this.run}>run</button>
</div>
)
}

}

export default ErrorHandleServerFunctions
Loading
Loading