Skip to content

Content-negotiate by Accept quality, not JSON-only - #3601

Merged
pfefferle merged 5 commits into
trunkfrom
fix/accept-quality-negotiation
Aug 3, 2026
Merged

pfefferle merged 5 commits into
trunkfrom
fix/accept-quality-negotiation

Conversation

@pfefferle

@pfefferle pfefferle commented Aug 2, 2026 •

Copy link
Copy Markdown
Member

Proposed changes:

9.2.0 tightened content negotiation to treat a request as ActivityPub only when the Accept header was JSON and nothing else. Mastodon (and other software) send text/html;q=0.1 as a low-priority fallback alongside the ActivityPub types, so that check stopped recognizing them and served the HTML page instead, which broke lookups and federation.

This replaces it with a quality-aware, ActivityPub-specific check, accept_prefers_activitypub():

  • It ranks the Accept media types by q (ties broken by the order they appear) and looks at the winner.
  • The winner counts as ActivityPub only if it is application/activity+json, or application/ld+json carrying the ActivityStreams 2.0 profile (matched without the scheme, so http:// and https:// both work). Plain application/json, bare application/ld+json, and other *+json types are not ActivityPub.
  • A q=0 refuses a type and is ignored.

So Mastodon (ActivityPub at q=1, text/html at q=0.1) gets ActivityPub again; a browser (text/html at q=1) gets HTML.

The Surge cache drop-in calls the same helper. The LiteSpeed .htaccess rule and the Varnish/Nginx doc examples can't compare quality values, so they approximate by matching when the first media type is ActivityPub, which equals the q-value result for every real client.

Other information:

  • Have you written new tests for your changes, if applicable?

Testing instructions:

  • Publish a post and confirm it resolves when you paste its URL into Mastodon search.
  • curl -H 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/activity+json, text/html;q=0.1' <post-url> returns application/activity+json (Mastodon's exact header).
  • A normal browser request still returns the HTML page.

Copilot AI review requested due to automatic review settings August 2, 2026 08:34
@pfefferle pfefferle self-assigned this Aug 2, 2026
@pfefferle
pfefferle requested a review from a team August 2, 2026 08:34
@github-actions github-actions Bot added [Focus] Compatibility Ensuring the plugin plays well with other plugins [Tests] Includes Tests labels Aug 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates ActivityPub content negotiation to correctly handle real-world Accept headers (notably Mastodon’s JSON types plus low-q HTML fallback), so federation requests receive JSON again while browsers still get HTML.

Changes:

  • Replace the previous “JSON-only” Accept-header check with a q-aware accept_prefers_json() helper and wire it into Query::is_activitypub_request().
  • Update cache integrations (Surge + LiteSpeed) and related tests to follow the new negotiation behavior.
  • Add a changelog entry describing the federation lookup fix.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
includes/functions-request.php Introduces accept_prefers_json() helper for quality-aware content negotiation.
includes/class-query.php Switches ActivityPub request detection to use accept_prefers_json() on the raw Accept header.
integration/surge-cache-config.php Aligns Surge cache bucketing with the new Accept-header negotiation helper.
integration/class-litespeed-cache.php Simplifies LiteSpeed RewriteCond to approximate “JSON preferred” based on first media type.
tests/phpunit/tests/includes/class-test-functions-request.php Updates unit tests/data provider to cover q-aware Accept parsing behavior (including Mastodon-style headers).
tests/phpunit/tests/integration/class-test-litespeed-cache.php Updates integration test to compare htaccess Accept matching against accept_prefers_json().
.github/changelog/fix-accept-quality-negotiation Adds patch-level changelog entry for Mastodon/Fediverse lookup regression fix.

Comment thread includes/functions-request.php Outdated
Comment thread integration/class-litespeed-cache.php Outdated
Comment thread tests/phpunit/tests/integration/class-test-litespeed-cache.php
@github-actions github-actions Bot added the Docs label Aug 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

docs/how-to/caching.md:96

  • The LiteSpeed manual .htaccess snippet uses a regex that matches any application/ld+json whose parameters contain activitystreams, which can diverge from accept_prefers_activitypub() for non-W3C profile URIs. Tighten the snippet to require www.w3.org/ns/activitystreams to match the plugin logic.
RewriteCond %{HTTP:Accept} ^[\s,]*(application/activity\+json|application/ld\+json[^,]*activitystreams) [NC]
RewriteRule ^ - [E=Cache-Control:vary=%{ENV:LSCACHE_VARY_VALUE}+isjson]

docs/how-to/caching.md:75

  • The Nginx map example regex matches any application/ld+json whose parameters contain activitystreams, even if the profile is not the W3C ActivityStreams namespace. Tighten it to require www.w3.org/ns/activitystreams so the doc example aligns with accept_prefers_activitypub().
    "~*^[\s,]*(application/activity\+json|application/ld\+json[^,]*activitystreams)"   "json";

tests/phpunit/tests/integration/class-test-litespeed-cache.php:149

  • This test suite doesn’t currently cover the case where application/ld+json has a non-W3C profile URI that still contains the substring activitystreams (e.g. https://example.com/activitystreams). The current .htaccess regex would match such a header, but accept_prefers_activitypub() would not, so adding an explicit false case here helps prevent cache-bucket drift.
			'application/json'                     => false,
			'application/ld+json'                  => false,
			'application/ld+json; profile="https://example.com/ns"' => false,

docs/how-to/caching.md:60

  • The Varnish example regex matches any application/ld+json whose parameters contain activitystreams, even if the profile is not the W3C ActivityStreams namespace. That can mis-bucket caches compared to the plugin’s accept_prefers_activitypub() logic. Tightening the regex to require www.w3.org/ns/activitystreams keeps the example aligned with the plugin.

This issue also appears in the following locations of the same file:

  • line 75
  • line 95
    if (req.http.Accept ~ "(?i)^[\s,]*(application/activity\+json|application/ld\+json[^,]*activitystreams)") {
        hash_data("activitypub");
    }

Comment thread integration/class-litespeed-cache.php
@pfefferle
pfefferle merged commit c4800c6 into trunk Aug 3, 2026
11 checks passed
@pfefferle
pfefferle deleted the fix/accept-quality-negotiation branch August 3, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Docs [Focus] Compatibility Ensuring the plugin plays well with other plugins [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants