Empty `discourse_permalink` Causes Silent Comment Sync Failures

Environment

  • WP-Discourse version: 2.5.7

  • Discourse version: 3.5.0.beta5-dev

  • WordPress version: 6.9

  • Hosting: WP Engine (WordPress multisite)

  • Discourse instance: Self-hosted / Discourse hosting (forum.avweb.com)

Summary

Posts published from WordPress successfully create topics on Discourse, but discourse_permalink is stored as an empty string in post meta. This causes all subsequent comment syncs to silently fail, as sync_comments() in discourse-comment.php bails early when the permalink is empty. The Discourse topic ID and webhook sync flag are written correctly — only the permalink is missing.

We identified 174 affected posts on a single site in our multisite network. The issue appears to have started around mid-December 2025.

Steps to Reproduce

  1. Publish a WordPress post configured to create a Discourse topic

  2. Topic is created successfully on Discourse

  3. discourse_topic_id is saved to post meta ✓

  4. wpdc_sync_post_comments is set to 1 (webhook fires correctly) ✓

  5. discourse_permalink is saved as an empty string ✗

  6. discourse_comments_raw is never written (sync never completes)

Expected Behavior

discourse_permalink should contain the full Discourse topic URL (e.g., The China Chickens Come Home - AVweb - News Discussion - AVweb.com Discussion ) after successful topic creation.

Actual Behavior

discourse_permalink exists as a meta key but its value is empty. Every subsequent call to sync_comments() hits this guard on line 209 of lib/discourse-comment.php and returns early:

if ( ! $discourse_permalink ) {
    return 0;
}

Because the sync never completes, discourse_last_sync is never written, so the plugin retries on every page load — and fails every time.

Diagnosis

We traced the issue through the following code path:

  1. DiscourseCommentFormatter::format() calls do_action('wpdc_sync_discourse_comments') which triggers DiscourseComment::sync_comments()

  2. sync_comments() checks for discourse_permalink — finds it empty, returns 0

  3. format() then checks for discourse_comments_raw in post custom — finds it missing, returns bad_response_html()

  4. Comments never display for the affected post

The topic creation flow is writing discourse_topic_id but failing to persist the permalink. We were able to reconstruct the correct permalinks by querying the Discourse API at /t/{topic_id}.json and writing them back to post meta, which resolved the sync for all 174 posts.

Workaround

We wrote a WP-CLI repair script that:

  1. Finds posts where wpdc_sync_post_comments = 1 but discourse_comments_raw is missing

  2. Fetches the topic slug from /t/{topic_id}.json

  3. Reconstructs and saves the permalink

  4. Fetches and saves comments from /t/{slug}/{topic_id}/wordpress.json

We’re running this on a scheduled cron as a stopgap.


Additional Issues Found During Investigation

1. Global MySQL Lock Causes Silent Sync Failures

File: lib/discourse-comment.php, line 176

$got_lock = $wpdb->get_row( "SELECT GET_LOCK( 'discourse_lock', 0 ) got_it" );

sync_comments() uses a single global MySQL lock (discourse_lock) shared across all posts on the entire installation. The timeout is 0 (non-blocking), so if any post is currently syncing, all other posts silently skip their sync — no logging, no retry.

On a high-volume site publishing multiple posts per day, this creates a race condition where posts consistently lose the lock and never sync. Combined with the 10-minute sync period, a post can get permanently stuck if it loses the lock on its first few attempts and then the empty permalink issue prevents future syncs.

Suggested fix: Use a per-post lock:

$got_lock = $wpdb->get_row( "SELECT GET_LOCK( 'discourse_lock_{$post_id}', 0 ) got_it" );

With the corresponding release:

$wpdb->get_results( "SELECT RELEASE_LOCK( 'discourse_lock_{$post_id}' )" );

2. Double-Escaping of discourse_comments_raw

File: lib/discourse-comment.php, line 222

update_post_meta( $post_id, 'discourse_comments_raw', esc_sql( $raw_body ) );

update_post_meta() uses $wpdb->prepare() internally, so wrapping the value in esc_sql() causes double-escaping. Over multiple sync cycles, quotes in the JSON body accumulate escape characters (\"\\\"\\\\\\\") until the JSON becomes unparseable.

Suggested fix:

update_post_meta( $post_id, 'discourse_comments_raw', $raw_body );

Impact

These issues compound: the empty permalink prevents initial sync, the global lock prevents recovery, and the double-escaping can corrupt data for posts that do manage to sync. On our installation, 174 posts (roughly 2 months of content) were affected before we identified the root cause.