From 7733369250b44a2a21a7f474537451f7af495457 Mon Sep 17 00:00:00 2001 From: Ben Word Date: Sat, 18 Apr 2026 11:21:13 -0500 Subject: [PATCH] Return 406 Not Acceptable for unsupported Accept headers --- README.md | 10 +++++++++ post-content-to-markdown.php | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/README.md b/README.md index bcff200..5c41319 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,16 @@ add_filter('post_content_to_markdown/post_allowed', function ($allowed, $post) { **Default:** `true` +#### `post_content_to_markdown/strict_accept` + +Controls whether the plugin returns `406 Not Acceptable` when the client's `Accept` header rules out every representation the site can serve (i.e. neither `text/html` nor `text/markdown` is acceptable). Enabled by default for spec-correct content negotiation. Disable if you'd rather always fall back to HTML for any `Accept` value. + +```php +add_filter('post_content_to_markdown/strict_accept', '__return_false'); +``` + +**Default:** `true` + #### `post_content_to_markdown/emit_vary` Controls whether the plugin appends `Vary: Accept` to every front-end response. Enabled by default so that downstream caches (browsers, proxies, CDNs) key on the Accept header and don't cross-serve an HTML response to a Markdown request (or vice versa). Disable if you don't want the plugin touching HTML response headers — Markdown responses will still include `Vary: Accept` regardless, so content negotiation stays correct for direct Markdown hits. diff --git a/post-content-to-markdown.php b/post-content-to-markdown.php index 4452114..8b318c8 100644 --- a/post-content-to-markdown.php +++ b/post-content-to-markdown.php @@ -91,6 +91,22 @@ function isMarkdownRequested() return $markdown > 0.0 && $markdown > $html; } +/** + * Whether the client's Accept header allows any representation we can serve + * (text/html or text/markdown). Missing Accept counts as acceptable per + * RFC 9110 (absence means any type is acceptable). + */ +function isAcceptable() +{ + $accept = $_SERVER['HTTP_ACCEPT'] ?? ''; + if ($accept === '') { + return true; + } + + return acceptQuality($accept, 'text', 'html') > 0.0 + || acceptQuality($accept, 'text', 'markdown') > 0.0; +} + /** * Tell caching plugins (WP Super Cache, etc.) to skip caching for markdown requests. * This runs early to ensure the constant is set before caching plugins check it. @@ -118,6 +134,31 @@ function isMarkdownRequested() header('Vary: Accept', false); }); +/** + * Return 406 Not Acceptable when the client's Accept header rules out every + * representation we can serve. Disable via the strict_accept filter to fall + * back to HTML instead. + */ +add_action('template_redirect', function () { + if (is_feed() || is_admin()) { + return; + } + + if (! apply_filters('post_content_to_markdown/strict_accept', true)) { + return; + } + + if (isAcceptable()) { + return; + } + + status_header(406); + header('Content-Type: text/plain; charset=utf-8'); + header('Vary: Accept', false); + echo "406 Not Acceptable\n\nAvailable representations: text/html, text/markdown\n"; + exit; +}, 1); + /** * Serve content as Markdown based on Accept header or query parameter */