Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/nitro-skip-default-log-expected-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'evlog': patch
---

Stop Nitro and Nuxt from printing expected `createError` 4xx responses as `[request error] [unhandled]` in production. The wide event already records these errors, so evlog's Nitro error handler now skips Nitro's default logging for `EvlogError` with a status below 500. Server errors and errors that are not from evlog are still logged by Nitro.
3 changes: 2 additions & 1 deletion packages/evlog/src/nitro-v3/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
extractErrorStatus,
buildPlainNitroErrorBody,
isSensitiveNitroError,
isExpectedEvlogError,
serializeEvlogErrorResponse,
shouldSerializeNitroErrorAsJson,
shouldSuppressNitroDevOverlay,
Expand Down Expand Up @@ -48,7 +49,7 @@ export default defineErrorHandler(async (error, event, ctx: NitroErrorHandlerCon

const suppressOverlay = shouldSuppressNitroDevOverlay()

if (!suppressOverlay) {
if (!suppressOverlay && !isExpectedEvlogError(evlogError)) {
await ctx.defaultHandler(error, event, { silent: false })
}

Expand Down
9 changes: 9 additions & 0 deletions packages/evlog/src/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ export function resolveEvlogError(error: Error): Error | null {

export { extractErrorStatus } from './shared/errors'

/**
* Whether an error is an EvlogError with a 4xx status: an expected outcome that
* the wide event already records, so Nitro's default logging is skipped for it.
* @internal
*/
export function isExpectedEvlogError(evlogError: Error | null): boolean {
return evlogError !== null && extractErrorStatus(evlogError) < 500
}

/** Request metadata used to decide JSON vs framework error-page rendering. */
export interface NitroErrorRequestContext {
pathname: string
Expand Down
3 changes: 2 additions & 1 deletion packages/evlog/src/nitro/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
extractErrorStatus,
buildPlainNitroErrorBody,
isSensitiveNitroError,
isExpectedEvlogError,
serializeEvlogErrorResponse,
markH3ErrorHandled,
shouldSerializeNitroErrorAsJson,
Expand Down Expand Up @@ -55,7 +56,7 @@ export default defineNitroErrorHandler(async (error, event, ctx) => {

// Nitro v2 always passes `ctx`, but a missing context (e.g. the handler
// invoked directly) must degrade to a flushed response, not a crash.
if (!suppressOverlay && ctx?.defaultHandler) {
if (!suppressOverlay && !isExpectedEvlogError(evlogError) && ctx?.defaultHandler) {
await ctx.defaultHandler(error, event, { silent: false })
}

Expand Down
28 changes: 28 additions & 0 deletions packages/evlog/test/nitro-v3/errorHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,34 @@ describe('nitro-v3 errorHandler', () => {
expect(body.message).toBe('Invalid email format')
expect(body.statusMessage).toBe('Invalid email format')
})

it('skips Nitro default logging for an expected 4xx EvlogError in production', async () => {
vi.stubEnv('NODE_ENV', 'production')

const evlogError = Object.assign(new Error('Payment failed'), {
name: 'EvlogError',
status: 402,
data: { why: 'Card declined', fix: 'Use another card' },
})

const response = await invokeErrorHandler(evlogError)

expect(defaultHandlerMock).not.toHaveBeenCalled()
expect(response?.status).toBe(402)
})

it('keeps Nitro default logging for an EvlogError with a 5xx status in production', async () => {
vi.stubEnv('NODE_ENV', 'production')

const evlogError = Object.assign(new Error('Payment provider unavailable'), {
name: 'EvlogError',
status: 503,
})

await invokeErrorHandler(evlogError)

expect(defaultHandlerMock).toHaveBeenCalledWith(evlogError, mockEvent, { silent: false })
})
})

describe('HTML page delegation (#390)', () => {
Expand Down
28 changes: 28 additions & 0 deletions packages/evlog/test/nitro/errorHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,34 @@ describe('errorHandler', () => {
expect(sentBody.message).toBe('Invalid email format')
expect(sentBody.statusMessage).toBe('Invalid email format')
})

it('skips Nitro default logging for an expected 4xx EvlogError in production', async () => {
vi.stubEnv('NODE_ENV', 'production')

const evlogError = Object.assign(new Error('Payment failed'), {
name: 'EvlogError',
status: 402,
data: { why: 'Card declined', fix: 'Use another card' },
})

await invokeErrorHandler(evlogError)

expect(defaultHandlerMock).not.toHaveBeenCalled()
expect(readResponseBody().statusCode).toBe(402)
})

it('keeps Nitro default logging for an EvlogError with a 5xx status in production', async () => {
vi.stubEnv('NODE_ENV', 'production')

const evlogError = Object.assign(new Error('Payment provider unavailable'), {
name: 'EvlogError',
status: 503,
})

await invokeErrorHandler(evlogError)

expect(defaultHandlerMock).toHaveBeenCalledWith(evlogError, mockEvent, { silent: false })
})
})

describe('HTML page delegation (#390)', () => {
Expand Down
Loading