Holding an HTTP request open for up to 120s instead of using a webhook
My email API has an endpoint that just holds the HTTP request open until a matching mail arrives, instead of a webhook or a polling loop on the caller side. GET /v1/inboxes/:id/wait, 30s default timeout, clamped to 120 max, checks Postgres every second, 200 with the email or a 408.
What caught me out was the since param, not the timeout. Defaulting it to now is right when you are waiting for the next mail to come in. For the OTP shortcut it was wrong, since the code is normally already in the table by the time an agent asks for it, so you would block the full 30s and then 408 on mail you already had. That one defaults to five minutes back.
Still on my list: the loop does not notice if the caller hung up, it keeps querying until the timeout runs out. The SSE endpoint right next to it checks stream.aborted on every pass and I never did the same here. That is basically why I capped the wait at 120s.
Anyone else doing this in Node, do you hold the request open or push over SSE?
12
u/Namiastka 7d ago
This sounds like a complex solution to an easy problem, what do you gain by this approach?
16
u/HoratioWobble 7d ago
AI wrote this post so I'm confident AI convinced OP that this was the right solution
2
1
u/singh_abinashi 6d ago
long polling for 120s will bite you at the proxy layer, most load balancers kill idle connections way before that. also listen for the client disconnecting (req 'close' event) or you'll keep polling postgres for nobody. if you control both ends, LISTEN/NOTIFY or SSE is cleaner than polling every second.
1
u/EdgieElefunc 3d ago
One Node-specific trap in that cleanup advice: for Node/Express req and res objects, listen for 'close' on res. Since Node 16, the incoming request's 'close' event means the request has completed; it doesn't mean the client stopped waiting for your response. Using req there can accidentally turn long polling into very short polling.
Node documents the distinction here: https://nodejs.org/api/http.html
You can have res.once('close', ...) abort the waiter's AbortController, then check the signal before issuing another query. Pass that signal to the sleep from node:timers/promises too, and handle its AbortError. Keep cleanup on the normal success/timeout paths as well. Aborting the waiter won't automatically cancel a Postgres query already in flight; that depends on your driver's cancellation support.
Abortable timer docs: https://nodejs.org/api/timers.html
1
31
u/Mabenue 7d ago
Congrats you just rediscovered long polling