Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Set cache headers on WordPress 404 pages.
<?php
/**
* Force cache headers on 404 pages and prevent WordPress from handling 404s.
*
* @param bool $preempt determines who handles 404s.
* @param obj $wp_query global query object.
*/
function change_404_headers( $preempt, $wp_query ) {
if ( ! is_admin() && ! is_robots() && count( $wp_query->posts ) < 1 ) {
header( 'Cache-Control: max-age=30000, must-revalidate' );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', strtotime( '+5000 minutes' ) ) . ' GMT' );
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', strtotime( '-5000 minutes' ) ) . ' GMT' );
$wp_query->set_404();
status_header( 404 );
// prevents the default 404 from firing.
return true;
}
return $preempt;
}
add_filter( 'pre_handle_404', 'change_404_headers', 2, 99 );
@MikeNGarrett

This comment has been minimized.

Copy link
Owner Author

@MikeNGarrett MikeNGarrett commented May 8, 2018

Please note, this has uncovered some unexpected behavior in 4.9 and should not be used. A bug has been filed to get to the bottom of things. I'll update this gist as the ticket progresses.

@kshaner

This comment has been minimized.

Copy link

@kshaner kshaner commented May 9, 2018

The issue discovered is when the site is using a permalink structure where the first variable is one of: postname, category, tag, or author. This throws WP_Rewrite into 'use_verbose_page_rules' mode. This means that WP_Query is always in a state of error because no rewrite rule matches but the query still runs and performs a standard blog query where the most recent posts are returned but the query still calls $wp_query->set_404. This causes the count( $wp_query->posts ) < 1 check to fail.

Also, the action and argument count arguments are inverted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment