Skip to content

Commit ec03e5d

Browse files
authored
fix(nextjs): Revert #4139 - remove manipulation of res.finished value (#4516)
In #4139, a change was introduced in order to suppress a false positive warning thrown by nextjs, by setting `res.finished` to `true` just long enough for nextjs to check it and decide no warning was needed. In simple cases, it worked just fine, but in some set-ups the "just long enough for nextjs to check it and calm down" turned out to also be "just long enough for other things to check it and get mad." This backs out that change, as it seems it's doing more harm than good. In order to address the original problem (documented in [1] and [2]), a new warning is added, explaining that the false positive is just that. Not as elegant as suppressing the message altogether, but it should tide us over until such time as we're able to try again with a different approach. Fixes #4151. [1] #3852 [2] #4007
1 parent fa992bd commit ec03e5d

File tree

1 file changed

+11
-54
lines changed

1 file changed

+11
-54
lines changed

packages/nextjs/src/utils/withSentry.ts

+11-54
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)