@@ -74,7 +74,57 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
74
74
}
75
75
76
76
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 ;
78
128
} catch ( e ) {
79
129
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
80
130
// 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 =
112
162
type ResponseEndMethod = AugmentedResponse [ 'end' ] ;
113
163
type WrappedResponseEndMethod = AugmentedResponse [ 'end' ] ;
114
164
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
+ */
115
175
function wrapEndMethod ( origEnd : ResponseEndMethod ) : WrappedResponseEndMethod {
116
176
return async function newEnd ( this : AugmentedResponse , ...args : unknown [ ] ) {
117
177
const transaction = this . __sentryTransaction ;
@@ -140,6 +200,11 @@ function wrapEndMethod(origEnd: ResponseEndMethod): WrappedResponseEndMethod {
140
200
logger . log ( `Error while flushing events:\n${ e } ` ) ;
141
201
}
142
202
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
+
143
208
return origEnd . call ( this , ...args ) ;
144
209
} ;
145
210
}
0 commit comments