diff --git a/.github/workflows/aws-lambda.yml b/.github/workflows/aws-lambda.yml index da21a85bd9..561b4fe0f5 100644 --- a/.github/workflows/aws-lambda.yml +++ b/.github/workflows/aws-lambda.yml @@ -38,3 +38,11 @@ jobs: cd test/aws-lambda bundle install bundle exec maze-runner + + - name: Upload test artifacts + if: failure() + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: aws-lambda-test-results + path: test/aws-lambda/maze_output/ + if-no-files-found: ignore diff --git a/CHANGELOG.md b/CHANGELOG.md index c7a2405a89..241c967576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Fixed + +- (plugin-hono) Fix issue where error handler middleware did not `await next()` [#2735](https://github.com/bugsnag/bugsnag-js/pull/2735) + ## [8.9.0] - 2026-04-08 ### Fixed diff --git a/packages/plugin-hono/src/hono.js b/packages/plugin-hono/src/hono.js index 340a43ed7b..b2e81b6e06 100644 --- a/packages/plugin-hono/src/hono.js +++ b/packages/plugin-hono/src/hono.js @@ -37,23 +37,36 @@ module.exports = { }) const errorHandler = createMiddleware(async (c, next) => { - next() + let rethrow = false - if (!c.error || !client._config.autoDetectErrors) return + try { + // Catch all thrown values from routes by awaiting next() inside a try/catch block. + // This also ensures non-Error throws are attached to the context and processed without causing the route to hang. + await next() + } catch (err) { + c.error = err + rethrow = true + } - const event = client.Event.create(c.error, false, handledState, 'hono middleware', 1) + if (!c.error) return - if (c.bugsnag) { - c.bugsnag._notify(event) - } else { - client._logger.warn( - 'c.bugsnag is not defined. Make sure the @bugsnag/plugin-hono requestHandler middleware is added first.' - ) - const { metadata, request } = await getRequestAndMetadataFromReq(c) - event.request = { ...event.request, ...request } - event.addMetadata('request', metadata) - client._notify(event) + if (client._config.autoDetectErrors) { + const event = client.Event.create(c.error, false, handledState, 'hono middleware', 1) + + if (c.bugsnag) { + c.bugsnag._notify(event) + } else { + client._logger.warn( + 'c.bugsnag is not defined. Make sure the @bugsnag/plugin-hono requestHandler middleware is added first.' + ) + const { metadata, request } = await getRequestAndMetadataFromReq(c) + event.request = { ...event.request, ...request } + event.addMetadata('request', metadata) + client._notify(event) + } } + + if (rethrow) throw c.error }) return { requestHandler, errorHandler } diff --git a/test/aws-lambda/features/promise-rejection.feature b/test/aws-lambda/features/promise-rejection.feature index 2224f42227..09763ac326 100644 --- a/test/aws-lambda/features/promise-rejection.feature +++ b/test/aws-lambda/features/promise-rejection.feature @@ -111,32 +111,3 @@ Scenario: promise rejections are reported when using hono Then the session is valid for the session reporting API version "1" for the "Bugsnag Node" notifier And the session "id" is not null And the session "startedAt" is a timestamp - -@hono-app -Scenario Outline: thrown non-error exceptions are reported when using hono - Given I setup the environment - When I invoke the "HonoFunction" lambda in "features/fixtures/hono-app" with the "events/throw-non-error.json" event - Then the lambda response "errorMessage" equals "1" - And the lambda response "errorType" equals "Runtime.UnhandledPromiseRejection" - And the lambda response "trace" is an array with 4 elements - And the lambda response "trace.0" equals "Runtime.UnhandledPromiseRejection: 1" - And the lambda response "body" is null - And the lambda response "statusCode" is null - And the SAM exit code equals 0 - When I wait to receive an error - Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier - And the event "unhandled" is true - And the event "severity" equals "error" - And the event "severityReason.type" equals "unhandledPromiseRejection" - And the exception "errorClass" equals "InvalidError" - And the exception "message" matches "unhandledRejection handler received a non-error\." - And the exception "type" equals "nodejs" - And the event "metaData.AWS Lambda context.functionName" equals "HonoFunction" - And the event "metaData.AWS Lambda context.awsRequestId" is not null - And the event "device.runtimeVersions.node" matches "^18\.\d+\.\d+$" - When I wait to receive a session - Then the session is valid for the session reporting API version "1" for the "Bugsnag Node" notifier - And the session "id" is not null - And the session "startedAt" is a timestamp - And the event "session.events.handled" equals 0 - And the event "session.events.unhandled" equals 1 diff --git a/test/aws-lambda/features/unhandled.feature b/test/aws-lambda/features/unhandled.feature index 733cc35893..2fdc087d6e 100644 --- a/test/aws-lambda/features/unhandled.feature +++ b/test/aws-lambda/features/unhandled.feature @@ -132,7 +132,7 @@ Scenario: unhandled asynchronous exceptions are reported when using serverless-e And the event "session.events.unhandled" equals 1 @hono-app -Scenario Outline: unhandled exceptions are reported when using hono +Scenario: unhandled exceptions are reported when using hono Given I setup the environment When I invoke the "HonoFunction" lambda in "features/fixtures/hono-app" with the "events/unhandled.json" event And the SAM exit code equals 0 @@ -156,7 +156,7 @@ Scenario Outline: unhandled exceptions are reported when using hono And the event "session.events.unhandled" equals 1 @hono-app -Scenario Outline: unhandled asynchronous exceptions are reported when using hono +Scenario: unhandled asynchronous exceptions are reported when using hono Given I setup the environment When I invoke the "HonoFunction" lambda in "features/fixtures/hono-app" with the "events/unhandled-async.json" event And the SAM exit code equals 0 @@ -179,3 +179,44 @@ Scenario Outline: unhandled asynchronous exceptions are reported when using hono And the event "session.events.handled" equals 0 And the event "session.events.unhandled" equals 1 +@hono-app +Scenario: thrown non-error exceptions are reported when using hono + Given I setup the environment + When I invoke the "HonoFunction" lambda in "features/fixtures/hono-app" with the "events/throw-non-error.json" event + Then the lambda response "errorMessage" equals "1" + And the lambda response "errorType" equals "number" + And the lambda response "body" is null + And the lambda response "statusCode" is null + And the SAM exit code equals 0 + When I wait to receive 2 errors + + Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier + And the event "unhandled" is true + And the event "severity" equals "error" + And the event "severityReason.type" equals "unhandledException" + And the exception "errorClass" equals "Error" + And the exception "message" equals "1" + And the exception "type" equals "nodejs" + And the event "metaData.AWS Lambda context.functionName" equals "HonoFunction" + And the event "metaData.AWS Lambda context.awsRequestId" is not null + And the event "device.runtimeVersions.node" matches "^18\.\d+\.\d+$" + + # Error thrown by hono + And I discard the oldest error + Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier + And the event "unhandled" is true + And the event "severity" equals "error" + And the event "severityReason.type" equals "unhandledErrorMiddleware" + And the exception "errorClass" equals "InvalidError" + And the exception "message" matches "hono middleware received a non-error\." + And the exception "type" equals "nodejs" + And the event "metaData.AWS Lambda context.functionName" equals "HonoFunction" + And the event "metaData.AWS Lambda context.awsRequestId" is not null + And the event "device.runtimeVersions.node" matches "^18\.\d+\.\d+$" + + When I wait to receive a session + Then the session is valid for the session reporting API version "1" for the "Bugsnag Node" notifier + And the session "id" is not null + And the session "startedAt" is a timestamp + And the event "session.events.handled" equals 0 + And the event "session.events.unhandled" equals 1 diff --git a/test/node/features/express.feature b/test/node/features/express.feature index 40b3732755..bafefe44c2 100644 --- a/test/node/features/express.feature +++ b/test/node/features/express.feature @@ -163,7 +163,7 @@ Scenario: an unhandled promise rejection in an async callback (without request c And the exception "message" equals "unhandled rejection in async callback" Scenario: adding body to request metadata - When I POST the data "data=in_request_body" to the URL "http://express/bodytest" + When I POST the data "data=in_request_body" to the URL "http://express/bodytest" with the content type "application/x-www-form-urlencoded" And I wait to receive an error Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier And the event "unhandled" is true @@ -215,7 +215,7 @@ Scenario: Context-aware console breadcrumbs And the event "request.clientIp" is not null Scenario: context loss - When I POST the data "some=body_data" to the URL "http://express/context-loss" + When I POST the data "some=body_data" to the URL "http://express/context-loss" with the content type "application/x-www-form-urlencoded" And I wait to receive an error Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier And the exception "errorClass" equals "Error" diff --git a/test/node/features/express_disabled.feature b/test/node/features/express_disabled.feature index 09be49325e..47bdd9956e 100644 --- a/test/node/features/express_disabled.feature +++ b/test/node/features/express_disabled.feature @@ -50,5 +50,5 @@ Scenario: a handled error passed to req.bugsnag.notify() And the event "request.clientIp" is not null Scenario: adding body to request metadata - When I POST the data "data=in_request_body" to the URL "http://express-disabled/bodytest" + When I POST the data "data=in_request_body" to the URL "http://express-disabled/bodytest" with the content type "application/x-www-form-urlencoded" And I should receive no errors diff --git a/test/node/features/feature_flags.feature b/test/node/features/feature_flags.feature index d865ac3199..4a691ea77b 100644 --- a/test/node/features/feature_flags.feature +++ b/test/node/features/feature_flags.feature @@ -9,7 +9,7 @@ Background: Scenario: adding feature flags for an unhandled error Given I start the service "express" And I wait for the host "express" to open port "80" - When I POST the data "a=1&b=2&c=3&d=4" to the URL "http://express/features/unhandled" + When I POST the data "a=1&b=2&c=3&d=4" to the URL "http://express/features/unhandled" with the content type "application/x-www-form-urlencoded" And I wait to receive an error Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier And the event "unhandled" is true @@ -29,7 +29,7 @@ Scenario: adding feature flags for an unhandled error | d | 4 | # ensure each request can have its own set of feature flags When I discard the oldest error - And I POST the data "x=9&y=8&z=7" to the URL "http://express/features/unhandled" + And I POST the data "x=9&y=8&z=7" to the URL "http://express/features/unhandled" with the content type "application/x-www-form-urlencoded" And I wait to receive an error And the event contains the following feature flags: | featureFlag | variant | @@ -42,7 +42,7 @@ Scenario: adding feature flags for an unhandled error Scenario: adding feature flags for a handled error Given I start the service "express" And I wait for the host "express" to open port "80" - When I POST the data "a=1&b=2&c=3&d=4" to the URL "http://express/features/handled" + When I POST the data "a=1&b=2&c=3&d=4" to the URL "http://express/features/handled" with the content type "application/x-www-form-urlencoded" And I wait to receive an error Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier And the event "unhandled" is false @@ -62,7 +62,7 @@ Scenario: adding feature flags for a handled error | d | 4 | # ensure each request can have its own set of feature flags When I discard the oldest error - And I POST the data "x=9&y=8&z=7" to the URL "http://express/features/handled" + And I POST the data "x=9&y=8&z=7" to the URL "http://express/features/handled" with the content type "application/x-www-form-urlencoded" And I wait to receive an error And the event contains the following feature flags: | featureFlag | variant | @@ -75,7 +75,7 @@ Scenario: adding feature flags for a handled error Scenario: clearing all feature flags doesn't affect subsequent requests Given I start the service "express" And I wait for the host "express" to open port "80" - When I POST the data "a=1&b=2&c=3&d=4&clearAllFeatureFlags" to the URL "http://express/features/unhandled" + When I POST the data "a=1&b=2&c=3&d=4&clearAllFeatureFlags" to the URL "http://express/features/unhandled" with the content type "application/x-www-form-urlencoded" And I wait to receive an error Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier And the event "unhandled" is true @@ -87,7 +87,7 @@ Scenario: clearing all feature flags doesn't affect subsequent requests And the event "request.httpMethod" equals "POST" And the event has no feature flags When I discard the oldest error - And I POST the data "x=9&y=8&z=7" to the URL "http://express/features/unhandled" + And I POST the data "x=9&y=8&z=7" to the URL "http://express/features/unhandled" with the content type "application/x-www-form-urlencoded" And I wait to receive an error And the event contains the following feature flags: | featureFlag | variant | diff --git a/test/node/features/fixtures/hono/scenarios/app.js b/test/node/features/fixtures/hono/scenarios/app.js index 805e4a5341..b82edcfc73 100644 --- a/test/node/features/fixtures/hono/scenarios/app.js +++ b/test/node/features/fixtures/hono/scenarios/app.js @@ -16,7 +16,6 @@ const app = new Hono(); const middleware = Bugsnag.getPlugin('hono') app.use(middleware.requestHandler) - app.use(middleware.errorHandler) app.get('/', (c) => { @@ -24,7 +23,7 @@ app.get('/', (c) => { }) app.get('/handled', async (c, next) => { - Bugsnag.notify(new Error('handled')); + c.bugsnag.notify(new Error('handled')); await next(); }); @@ -32,6 +31,7 @@ app.get('/sync', (c) => { throw new Error('sync') }) +// Causes the app to crash app.get('/async', (c) => { setTimeout(function () { throw new Error('async') @@ -48,10 +48,17 @@ app.get('/rejection-async', (c) => { }, 100) }) -app.get('/throw-non-error', async (c, next) => { +app.get('/throw-non-error', async (c) => { throw 1 }) +// Causes a 'Context is not finalized' Error if the error handler middleware does not `await next()` +app.post('/post-body', async (c) => { + await c.req.raw.json(); + c.bugsnag.notify(new Error('error in post body route')); + return c.json({ a: 'test' }); +}); + serve({ fetch: app.fetch, port: 80 diff --git a/test/node/features/hono.feature b/test/node/features/hono.feature index 10377dc1d6..2f32e2894a 100644 --- a/test/node/features/hono.feature +++ b/test/node/features/hono.feature @@ -40,6 +40,7 @@ Scenario: a synchronous thrown error in a route And the event "metaData.request.query.a" equals "1" And the event "metaData.request.query.b" equals "2c" +# This route will cause the app to crash Scenario: an asynchronous thrown error in a route Then I open the URL "http://hono/async" tolerating any error And I wait to receive an error @@ -90,9 +91,22 @@ Scenario: throwing non-Error error Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier And the event "unhandled" is true And the event "severity" equals "error" - And the event "severityReason.type" equals "unhandledPromiseRejection" + And the event "severityReason.type" equals "unhandledErrorMiddleware" And the exception "errorClass" equals "InvalidError" - And the exception "message" matches "unhandledRejection handler received a non-error\." + And the exception "message" matches "hono middleware received a non-error\." And the exception "type" equals "nodejs" And the event "request.url" equals "http://hono/throw-non-error" - And the event "request.httpMethod" equals "GET" \ No newline at end of file + And the event "request.httpMethod" equals "GET" + +Scenario: error handler awaits next() + When I POST the data "{\"a\":1,\"b\":2}" to the URL "http://hono/post-body" with the content type "application/json" + Then I wait to receive an error + Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier + And the event "unhandled" is false + And the event "severity" equals "warning" + And the exception "errorClass" equals "Error" + And the exception "message" matches "error in post body route" + And the exception "type" equals "nodejs" + And the "file" of stack frame 0 equals "scenarios/app.js" + And the event "request.url" equals "http://hono/post-body" + And the event "request.httpMethod" equals "POST" diff --git a/test/node/features/koa.feature b/test/node/features/koa.feature index cb974eabfa..58297b2441 100644 --- a/test/node/features/koa.feature +++ b/test/node/features/koa.feature @@ -114,7 +114,7 @@ Scenario: A handled error with ctx.bugsnag.notify() And the event "metaData.error_handler.after" is null Scenario: adding body to request metadata - When I POST the data "data=in_request_body" to the URL "http://koa/bodytest" + When I POST the data "data=in_request_body" to the URL "http://koa/bodytest" with the content type "application/x-www-form-urlencoded" And I wait to receive an error Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier And the event "unhandled" is true diff --git a/test/node/features/koa_disabled.feature b/test/node/features/koa_disabled.feature index 3d1aa969c5..d19ee8ed42 100644 --- a/test/node/features/koa_disabled.feature +++ b/test/node/features/koa_disabled.feature @@ -46,5 +46,5 @@ Scenario: A handled error with ctx.bugsnag.notify() And the event "request.clientIp" is not null Scenario: adding body to request metadata - When I POST the data "data=in_request_body" to the URL "http://koa-disabled/bodytest" + When I POST the data "data=in_request_body" to the URL "http://koa-disabled/bodytest" with the content type "application/x-www-form-urlencoded" And I should receive no errors diff --git a/test/node/features/steps/server_fixture_request_steps.rb b/test/node/features/steps/server_fixture_request_steps.rb index 657576014e..571c00ce4e 100644 --- a/test/node/features/steps/server_fixture_request_steps.rb +++ b/test/node/features/steps/server_fixture_request_steps.rb @@ -1,13 +1,14 @@ require 'net/http' -# Attempts to POST a string of urlencoded data to a server. +# Attempts to POST a string of request body data to a server. # -# @step_input reqbody [String] urlencoded data to send. +# @step_input reqbody [String] body data to send. # @step_input url [String] The URL to post data to. -When("I POST the data {string} to the URL {string}") do |reqbody, url| +# @step_input content_type [String] The content type of the data being sent. +When("I POST the data {string} to the URL {string} with the content type {string}") do |reqbody, url, content_type| Net::HTTP.post(URI(url), reqbody, - 'Content-Type' => 'application/x-www-form-urlencoded') + 'Content-Type' => content_type) end When('I open the URL {string} tolerating any error') do |url|