@@ -74,7 +74,57 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
7474 }
7575
7676 try {
77- return await origHandler ( req , res ) ;
77+ const handlerResult = await origHandler ( req , res ) ;
78+
79+ // Temporarily mark the response as finished, as a hack to get nextjs to not complain that we're coming back
80+ // from the handler successfully without `res.end()` having completed its work. This is necessary (and we know
81+ // we can do it safely) for a few reasons:
82+ //
83+ // - Normally, `res.end()` is sync and completes before the request handler returns, as part of the handler
84+ // sending data back to the client. As soon as the handler is done, nextjs checks to make sure that the
85+ // response is indeed finished. (`res.end()` signals this by setting `res.finished` to `true`.) If it isn't,
86+ // nextjs complains.("Warning: API resolved without sending a response for <url>.")
87+ //
88+ // - In order to prevent the lambda running the route handler from shutting down before we can send events to
89+ // Sentry, we monkeypatch `res.end()` so that we can call `flush()`, wait for it to finish, and only then
90+ // allow the response to be marked complete. This turns the normally-sync `res.end()` into an async function,
91+ // which isn't awaited because it's assumed to still be sync. As a result, nextjs runs the aforementioned
92+ // check before the now-async `res.end()` has had a chance to set `res.finished = false`, and therefore thinks
93+ // there's a problem when there's not.
94+ //
95+ // - In order to trick nextjs into not complaining, we can set `res.finished` to `true` before exiting the
96+ // handler. If we do that, though, `res.end()` gets mad because it thinks *it* should be the one to get to
97+ // mark the response complete. We therefore need to flip it back to `false` 1) after nextjs's check but 2)
98+ // before the original `res.end()` is called.
99+ //
100+ // - The second part is easy - we control when the original `res.end()` is called, so we can do the flipping
101+ // right beforehand and `res.end()` will be none the wiser.
102+ //
103+ // - The first part isn't as obvious. How do we know we won't end up with a race condition, such that the
104+ // flipping to `false` might happen before the check, negating the entire purpose of this hack? Fortunately,
105+ // before it's done, our async `res.end()` wrapper has to await a `setImmediate()` callback, guaranteeing its
106+ // run lasts at least until the next event loop. The check, on the other hand, happens synchronously,
107+ // immediately after the request handler (so in the same event loop). So as long as we wait to flip
108+ // `res.finished` back to `false` until after the `setImmediate` callback has run, we know we'll be safely in
109+ // the next event loop when we do so.
110+ //
111+ // And that does it! Nextjs doesn't complain about an unfinished response, `res.end()` doesn’t complain about an
112+ // already-finished response, and everyone wins.
113+ //
114+ // One final note: It might seem like making `res.end()` an awaited async function would run the danger of
115+ // having the lambda close before it's done its thing, meaning we *still* might not get events sent to Sentry.
116+ // Fortunately, even though it's called `res.end()`, and even though it's normally sync, a) it's far from the
117+ // end of the request process, so there's other stuff which needs to happen before the lambda can close in any
118+ // case, and b) that other stuff isn't triggered until `res.end()` emits a `prefinished` event, so even though
119+ // it's not technically awaited, it's still the case that the process can't go on until it's done.
120+ //
121+ // See
122+ // https://github.com/vercel/next.js/blob/e1464ae5a5061ae83ad015018d4afe41f91978b6/packages/next/server/api-utils.ts#L106-L118
123+ // and
124+ // https://github.com/nodejs/node/blob/d8f1823d5fca5e3c00b19530fb15343fdd3c8bf5/lib/_http_outgoing.js#L833-L911.
125+ res . finished = true ;
126+
127+ return handlerResult ;
78128 } catch ( e ) {
79129 // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
80130 // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced
@@ -112,6 +162,16 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
112162type ResponseEndMethod = AugmentedResponse [ 'end' ] ;
113163type WrappedResponseEndMethod = AugmentedResponse [ 'end' ] ;
114164
165+ /**
166+ * Wrap `res.end()` so that it closes the transaction and flushes events before letting letting the request finish.
167+ *
168+ * Note: This wraps a sync method with an async method. While in general that's not a great idea in terms of keeping
169+ * things in the right order, in this case it's safe', as explained in detail in the long comment in the main
170+ * `withSentry()` function.
171+ *
172+ * @param origEnd The original `res.end()` method
173+ * @returns The wrapped version
174+ */
115175function wrapEndMethod ( origEnd : ResponseEndMethod ) : WrappedResponseEndMethod {
116176 return async function newEnd ( this : AugmentedResponse , ...args : unknown [ ] ) {
117177 const transaction = this . __sentryTransaction ;
@@ -140,6 +200,11 @@ function wrapEndMethod(origEnd: ResponseEndMethod): WrappedResponseEndMethod {
140200 logger . log ( `Error while flushing events:\n${ e } ` ) ;
141201 }
142202
203+ // If the request didn't error, we will have temporarily marked the response finished to avoid a nextjs error
204+ // message. (See long note above.) Now we need to flip `finished` back to `false` so that the real `res.end()`
205+ // method doesn't throw `ERR_STREAM_WRITE_AFTER_END` (which it will if presented with an already-finished response).
206+ this . finished = false ;
207+
143208 return origEnd . call ( this , ...args ) ;
144209 } ;
145210}
0 commit comments