pascalav
pascalav•15mo ago

How to handle a user abort by Deno.serve

Hello, I have a question and discovered this Discord channel now. The Deno website has an example at: https://deno.land/manual@v1.36.4/runtime/http_server_apis#inspecting-the-incoming-request There is the note below: Be aware that the req.text() call can fail if the user hangs up the connection before the body is fully received. Make sure to handle this case. However, I have not found a sample how to catch this case. Can Deno detect that the connection has been terminated? I take the example from the Deno site and changed it a bit:
const abortController = new AbortController();
Deno.addSignalListener('SIGINT', () => {
abortController.abort();
});

Deno.serve(
{
port:3000,
signal: abortController.signal
},
async (req) => {
if (req.body) {
try {
console.log('Start');
const body = await req.formData();
for (const key of body.keys()) {
console.log(key);
}
console.log('Never reached, when user abort.');
} catch {
// Will not be triggered on abort.
console.log('Error on await req.formData()');
}
}

return new Response("Hello, World!");
}
);
const abortController = new AbortController();
Deno.addSignalListener('SIGINT', () => {
abortController.abort();
});

Deno.serve(
{
port:3000,
signal: abortController.signal
},
async (req) => {
if (req.body) {
try {
console.log('Start');
const body = await req.formData();
for (const key of body.keys()) {
console.log(key);
}
console.log('Never reached, when user abort.');
} catch {
// Will not be triggered on abort.
console.log('Error on await req.formData()');
}
}

return new Response("Hello, World!");
}
);
By the way, I added the "AbortHandler" for the test. If await req.formData() passes successfully, the server can be terminated with Ctrl + C in the terminal (Linux Mint). If the user cancels the "upload", after the Ctrl + C command is still running a process until I close the terminal. I think that is why Deno writes on the website, you have to catch this case. How to do this?
Deno
HTTP Server APIs | Manual | Deno
Deno currently has three HTTP Server APIs:
7 Replies
Deno AI Helper
Deno AI Helper•15mo ago
According to the documentation, when a user aborts a connection, it can cause methods that read from the request body, such as req.text(), req.json(), req.formData(), req.arrayBuffer(), req.body.getReader().read(), req.body.pipeTo(), etc., to fail. In your code, you are using req.formData(), which is one of the methods that can fail if the user aborts the connection. To handle this, you can use a try-catch block around the req.formData() call, which you have already done in your code. However, it seems like the catch block is not being triggered when the user aborts the connection. This might be due to the way the error is being thrown or propagated. Unfortunately, the documentation does not provide a clear answer on how to handle this specific case. It might be helpful to check the error message or error type when the req.formData() call fails, to see if it provides any clues about the user aborting the connection. Also, it's worth noting that the AbortController and AbortSignal you are using in your code are for aborting the server itself, not for handling user-aborted connections. The AbortSignal is passed to Deno.serve() to allow the server to be aborted, for example, when you press Ctrl + C in the terminal. I hope this helps! If you have any more questions, feel free to ask. 🦕
partisani
partisani•15mo ago
try using three backticks to format your code:
const abortController = new AbortController();
Deno.addSignalListener('SIGINT', () => {
abortController.abort();
});

Deno.serve(
{
port:3000,
signal: abortController.signal
},
async (req) => {
if (req.body) {
try {
console.log('Start');
const body = await req.formData();
for (const key of body.keys()) {
console.log(key);
}
console.log('Never reached, when user abort.');
} catch {
// Will not be triggered on abort.
console.log('Error on await req.formData()');
}
}

return new Response("Hello, World!");
}
);
const abortController = new AbortController();
Deno.addSignalListener('SIGINT', () => {
abortController.abort();
});

Deno.serve(
{
port:3000,
signal: abortController.signal
},
async (req) => {
if (req.body) {
try {
console.log('Start');
const body = await req.formData();
for (const key of body.keys()) {
console.log(key);
}
console.log('Never reached, when user abort.');
} catch {
// Will not be triggered on abort.
console.log('Error on await req.formData()');
}
}

return new Response("Hello, World!");
}
);
pascalav
pascalavOP•15mo ago
Formatting of the code has worked. Thanks for the hint. try ... catch doesn't work, because await req.formData() doesn't trigger it. Yes, I know. The AbortSignal abort the server itself. The reason I added it, for example, was that Ctrl + C terminates the server, but then some process is still running in Deno. And only if req.formData() is terminated normally, no process is running in Deno afterwards. So unfortunately the bot does not help here 😦
Deno AI Helper
Deno AI Helper•15mo ago
I'm sorry for the confusion. The documentation does not provide a clear way to handle the case where a user aborts the connection during the req.formData() call. It's possible that this is a limitation of the current Deno APIs. However, you might be able to work around this by using a timeout. You could use the AbortSignal.timeout(ms) method to create an AbortSignal that aborts after a certain amount of time. This could help to ensure that the req.formData() call does not hang indefinitely if the user aborts the connection. Here's an example of how you might use AbortSignal.timeout(ms):
const signal = AbortSignal.timeout(5000); // 5 seconds timeout

Deno.serve(
{
port:3000,
signal: signal
},
async (req) => {
if (req.body) {
try {
console.log('Start');
const body = await req.formData();
for (const key of body.keys()) {
console.log(key);
}
console.log('Never reached, when user abort.');
} catch {
// Will not be triggered on abort.
console.log('Error on await req.formData()');
}
}

return new Response("Hello, World!");
}
);
const signal = AbortSignal.timeout(5000); // 5 seconds timeout

Deno.serve(
{
port:3000,
signal: signal
},
async (req) => {
if (req.body) {
try {
console.log('Start');
const body = await req.formData();
for (const key of body.keys()) {
console.log(key);
}
console.log('Never reached, when user abort.');
} catch {
// Will not be triggered on abort.
console.log('Error on await req.formData()');
}
}

return new Response("Hello, World!");
}
);
In this example, if the req.formData() call does not complete within 5 seconds, the AbortSignal will abort, and the req.formData() call should fail and throw an error, which you can catch in the catch block. Please note that this is just a workaround and might not work in all cases. The best solution would be for Deno to provide a way to handle user-aborted connections directly in the API.
mmastrac
mmastrac•15mo ago
This might be a bug on our end. I managed to create a repro for it
mmastrac
mmastrac•15mo ago
GitHub
Aborted requests do not throw · Issue #20502 · denoland/deno
If a connection hangs up before transmitting the body, request.text() should throw to indicate a failure. Deno.test( { permissions: { net: true } }, async function httpServerAbortedRequestBody() { ...
mmastrac
mmastrac•15mo ago
I just PR'd a fix as https://github.com/denoland/deno/pull/20503/. This will be working in the next release of Deno (next week, I believe).
GitHub
fix(ext/http): ensure aborted bodies throw by mmastrac · Pull Reque...
Fixes #20502 -- ensure that Hyper errors make it through to JS.