
Why you should cancel your HTTP requests
Have you ever built an autocomplete feature that felt slow or returned unexpected results? This often happens because of the way browsers handle HTTP requests. Here’s a simple autocomplete example you might have implemented:
import { useState, useEffect } from 'react';
function Autocomplete({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
try {
const res = await fetch(`/api/search?q=${query}`);
const data = await res.json();
setResults(data);
} catch (err) {
console.error(err);
}
}, [query]);
return (
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Why This is a Problem
Browsers using HTTP 1.1 typically only allow 6 simultaneous requests to the same domain. With an autocomplete feature, each keystroke triggers a new request. If a user types quickly, you’ll quickly hit this limit. This can cause:
Slow performance: Your autocomplete becomes slow because new requests have to wait for previous ones to finish.
Incorrect results (race conditions): Older requests that finish later might overwrite newer, more relevant results.
Simple Solution: Canceling Requests
The good news is, browsers let you cancel ongoing requests. You can easily add this capability to your autocomplete component using an AbortController. Here’s how you do it:
import { useState, useEffect } from 'react';
function Autocomplete({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
async function fetchResults() {
try {
const res = await fetch(`/api/search?q=${query}`,
{ signal: controller.signal });
const data = await res.json();
setResults(data);
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err);
}
}
}
fetchResults();
return () => controller.abort();
}, [query]);
return (
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Now, each new query cancels the previous request, ensuring only the latest data updates your component. Your autocomplete feature will feel faster and won’t have any race conditions.
The HTTP/2 solution
HTTP/2 can alleviate this by multiplexing multiple requests over a single TCP connection, greatly increasing concurrency. However, it’s not the default everywhere yet, and many systems still rely on HTTP 1.1.
Even when using HTTP/2 you should still cancel your HTTP requests to avoid race conditions.
Handling Cancellations on Your Server
If you want to go the extra mile, you can also handle canceled requests on your backend.
Here’s an example using Express:
import express from 'express';
const app = express();
app.get('/api/search', async (req, res) => {
const query = req.query.q;
const dbOperation = startDbSearch(query);
req.on('close', () => {
if (!res.writableEnded) {
dbOperation.cancel();
console.log('Request canceled by client');
}
});
try {
const results = await dbOperation;
res.json(results);
} catch (err) {
if (err.message !== 'Operation canceled') {
res.status(500).send(err.message);
}
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
By stopping expensive operations like DB queries you can reduce server load.
Understanding and implementing request cancellation can significantly enhance the responsiveness and efficiency of your web applications.