Changeset dd1028a in Klonkt


Ignore:
Timestamp:
06/24/2026 10:48:18 AM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
065452a
Parents:
5bf63b7
Message:

fix(activitypub): content-negotiation on canonical URLs

An AP request (Accept: application/activity+json) to a profile or post URL now
302-redirects to its /ap/* representation, so the same URL serves HTML to
browsers and AP-JSON to servers. Fixes Mastodon failing to resolve a pasted
profile/post URL (it received text/html). Gated on apWants() — no cost for
normal browser traffic.

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/server.js

    r5bf63b7 rdd1028a  
    6565import ogRoutes from './routes/og.js';
    6666import apRoutes from './routes/activitypub.js';
     67import { apWants } from './services/ActivityPubService.js';
    6768
    6869// SESSION_SECRET: use the env var if set. Otherwise auto-generate a strong one
     
    235236app.use(loadTheme);
    236237
     238// ActivityPub content negotiation on the human URLs: an AP request (Accept:
     239// application/activity+json) to a profile/post URL is redirected to its /ap/*
     240// representation — same URL serves HTML to browsers, AP-JSON to servers (this is
     241// how Mastodon resolves a pasted profile/post URL). Gated on apWants() so normal
     242// browser requests pay nothing.
     243app.use((req, res, next) => {
     244  if (req.method !== 'GET' || !apWants(req)) return next();
     245  const site = res.locals.site;
     246  if (!site || !site.slug) return next();
     247  const seg = req.path.replace(/^\/+|\/+$/g, '');
     248  if (seg === '') return res.redirect(302, `/ap/users/${encodeURIComponent(site.slug)}`);
     249  if (!seg.includes('/')) {
     250    try {
     251      const post = db.prepare(
     252        "SELECT id FROM posts WHERE site_id = ? AND slug = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)"
     253      ).get(site.id, seg);
     254      if (post) return res.redirect(302, `/ap/notes/${post.id}`);
     255    } catch { /* fall through to normal HTML handling */ }
     256  }
     257  return next();
     258});
     259
    237260// Lightweight CSRF defense: reject cross-origin state-mutating requests.
    238261// Same-origin forms + HTMX send a matching Origin; missing Origin is allowed
Note: See TracChangeset for help on using the changeset viewer.