@@ -79,53 +79,12 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
7979 try {
8080 const handlerResult = await origHandler ( req , res ) ;
8181
82- // Temporarily mark the response as finished, as a hack to get nextjs to not complain that we're coming back
83- // from the handler successfully without `res.end()` having completed its work. This is necessary (and we know
84- // we can do it safely) for a few reasons:
85- //
86- // - Normally, `res.end()` is sync and completes before the request handler returns, as part of the handler
87- // sending data back to the client. As soon as the handler is done, nextjs checks to make sure that the
88- // response is indeed finished. (`res.end()` signals this by setting `res.finished` to `true`.) If it isn't,
89- // nextjs complains. ("Warning: API resolved without sending a response for <url>.")
90- //
91- // - In order to prevent the lambda running the route handler from shutting down before we can send events to
92- // Sentry, we monkeypatch `res.end()` so that we can call `flush()`, wait for it to finish, and only then
93- // allow the response to be marked complete. This turns the normally-sync `res.end()` into an async function,
94- // which isn't awaited because it's assumed to still be sync. As a result, nextjs runs the aforementioned
95- // check before the now-async `res.end()` has had a chance to set `res.finished = false`, and therefore thinks
96- // there's a problem when there's not.
97- //
98- // - In order to trick nextjs into not complaining, we can set `res.finished` to `true` before exiting the
99- // handler. If we do that, though, `res.end()` gets mad because it thinks *it* should be the one to get to
100- // mark the response complete. We therefore need to flip it back to `false` 1) after nextjs's check but 2)
101- // before the original `res.end()` is called.
102- //
103- // - The second part is easy - we control when the original `res.end()` is called, so we can do the flipping
104- // right beforehand and `res.end()` will be none the wiser.
105- //
106- // - The first part isn't as obvious. How do we know we won't end up with a race condition, such that the
107- // flipping to `false` might happen before the check, negating the entire purpose of this hack? Fortunately,
108- // before it's done, our async `res.end()` wrapper has to await a `setImmediate()` callback, guaranteeing its
109- // run lasts at least until the next event loop. The check, on the other hand, happens synchronously,
110- // immediately after the request handler (so in the same event loop). So as long as we wait to flip
111- // `res.finished` back to `false` until after the `setImmediate` callback has run, we know we'll be safely in
112- // the next event loop when we do so.
113- //
114- // And with that, everybody's happy: Nextjs doesn't complain about an unfinished response, `res.end()` doesn’t
115- // complain about an already-finished response, and we have time to make sure events are flushed to Sentry.
116- //
117- // One final note: It might seem like making `res.end()` an awaited async function would run the danger of
118- // having the lambda close before it's done its thing, meaning we *still* might not get events sent to Sentry.
119- // Fortunately, even though it's called `res.end()`, and even though it's normally sync, a) it's far from the
120- // end of the request process, so there's other stuff which needs to happen before the lambda can close in any
121- // case, and b) that other stuff isn't triggered until `res.end()` emits a `prefinished` event, so even though
122- // it's not technically awaited, it's still the case that the process can't go on until it's done.
123- //
124- // See
125- // https://github.com/vercel/next.js/blob/e1464ae5a5061ae83ad015018d4afe41f91978b6/packages/next/server/api-utils.ts#L106-L118
126- // and
127- // https://github.com/nodejs/node/blob/d8f1823d5fca5e3c00b19530fb15343fdd3c8bf5/lib/_http_outgoing.js#L833-L911.
128- res . finished = true ;
82+ if ( process . env . NODE_ENV === 'development' ) {
83+ // eslint-disable-next-line no-console
84+ console . warn (
85+ '[sentry] If Next.js logs a warning "API resolved without sending a response", it\'s a false positive, which we\'re working to rectify.' ,
86+ ) ;
87+ }
12988
13089 return handlerResult ;
13190 } catch ( e ) {
@@ -183,8 +142,11 @@ type WrappedResponseEndMethod = AugmentedNextApiResponse['end'];
183142 * Wrap `res.end()` so that it closes the transaction and flushes events before letting the request finish.
184143 *
185144 * Note: This wraps a sync method with an async method. While in general that's not a great idea in terms of keeping
186- * things in the right order, in this case it's safe', as explained in detail in the long comment in the main
187- * `withSentry()` function.
145+ * things in the right order, in this case it's safe, because the native `.end()` actually *is* async, and its run
146+ * actually *is* awaited, just manually so (which reflects the fact that the core of the request/response code in Node
147+ * by far predates the introduction of `async`/`await`). When `.end()` is done, it emits the `prefinish` event, and
148+ * only once that fires does request processing continue. See
149+ * https://github.com/nodejs/node/commit/7c9b607048f13741173d397795bac37707405ba7.
188150 *
189151 * @param origEnd The original `res.end()` method
190152 * @returns The wrapped version
@@ -193,11 +155,6 @@ function wrapEndMethod(origEnd: ResponseEndMethod): WrappedResponseEndMethod {
193155 return async function newEnd ( this : AugmentedNextApiResponse , ...args : unknown [ ] ) {
194156 await finishSentryProcessing ( this ) ;
195157
196- // If the request didn't error, we will have temporarily marked the response finished to avoid a nextjs warning
197- // message. (See long note above.) Now we need to flip `finished` back to `false` so that the real `res.end()`
198- // method doesn't throw `ERR_STREAM_WRITE_AFTER_END` (which it will if presented with an already-finished response).
199- this . finished = false ;
200-
201158 return origEnd . call ( this , ...args ) ;
202159 } ;
203160}
0 commit comments