Stop Drupal Views deep pager URLs from breaking Solr with a 404
If a Drupal search page backed by Search API and Solr throws errors for URLs like /search?page=313361674, the cause is the pager arithmetic: Views multiplies the page number by the items per page to get Solr's start, and Solr parses that as a 32-bit integer. The fix is to throw a NotFoundHttpException for absurdly deep pages in hook_views_query_alter(), before the query is sent.
Why does a big page number cause a 400 from Solr?
Bots and crawlers sometimes request page numbers no human would reach. Views turns the page query parameter into an offset, which Search API passes to Solr as start. Solr reads start as a signed 32-bit integer, so anything above 2,147,483,647 fails to parse. The visible result is a 400 response with a NumberFormatException, and a noisy error log on every request.
Clamping the value is not enough. Silently serving the first page for an absurd request means crawlers see an endless set of URLs returning duplicate content. A 404 is the honest answer, and it tells crawlers to stop.
Where should the guard live?
A teammate put it in hook_views_query_alter(). That hook runs for every Views query, after the pager has been resolved, so it has the view, its display and the request. It also means the guard only applies to Views pagers. Endpoints that use a different convention, such as JSON:API's page[offset] and page[limit], never reach it.
A minimal version looks like this:
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
const MYMODULE_MAX_PAGE = 2000;
function mymodule_views_query_alter(ViewExecutable $view, QueryPluginBase $query): void {
mymodule_guard_pager_page($view);
}
function mymodule_guard_pager_page(ViewExecutable $view): void {
if (!$view->display_handler) {
return;
}
$pager = $view->display_handler->getPlugin('pager');
if (!$pager || !$pager->usePager()) {
return;
}
$page = $view->getRequest()->query->all()['page'] ?? NULL;
if (!is_scalar($page)) {
return;
}
// Views keeps one page per pager in a comma-separated list.
foreach (explode(',', (string) $page) as $value) {
$value = trim($value);
if ($value !== '' && ctype_digit($value) && (int) $value > MYMODULE_MAX_PAGE) {
throw new NotFoundHttpException();
}
}
}
What edge cases does the guard need to handle?
Three details made the difference between a guard that works and one that breaks other pages:
- Only act when a pager is in use. A display without a pager has no page value to validate, so the guard returns early.
- Handle the comma-separated form. Views stores one page per pager in
pageas a list, so every entry is checked, not just the first. - Leave junk alone. Empty and non-numeric values are left for Views to cast to page 0 as it already does. The guard only rejects values that are plain digits and above the cap.
Array values are also skipped, because a request such as ?page[offset]=1 is not Views' own parameter shape.
How do you choose the cap?
The cap only needs to be far below the overflow point and far above anything a real visitor reaches. The team used 2000. With a page size of, say, 20 items, page 2000 is offset 40,000, comfortably inside a 32-bit integer, and no human is paging that deep through search results. Pick a number for your own page sizes: the product of cap and the largest items-per-page setting must stay well below 2,147,483,647.
What breaks after you add the 404?
The follow-up bug was visual. The 404 subrequest keeps the original URL, so the theme still selected the search page's template and rendered the 404 node's title as a second hero above the 404 page's own heading. The fix was to skip the custom hero when the response status is 404. If your theme chooses templates from the request path, check what your error page looks like for a deep pager URL, not just that the status code is right.
Key takeaways
- Views multiplies the page number by items per page to produce Solr's
start, and Solr parses that as a 32-bit integer. - Throw a
NotFoundHttpExceptionfromhook_views_query_alter()for pages above a sensible cap, instead of clamping and serving duplicate content. - Check only plain digit values, handle the comma-separated list, and skip displays without a pager so the guard stays out of the way.
- Choose a cap so that cap times the largest page size stays far below the 32-bit limit.
- Test the 404 page itself, since a theme that selects templates from the request path can render the wrong hero.