Get rid of that annoying “WebSocket connection to ‘…’ failed” error on WordPress sites running Jetpack

If you run a self-hosted WordPress site with Jetpack, installed, you may have noticed a frequent error message showing up in your browser console.

In Chrome it looks like this:

WebSocket connection to ‘wss://public-api.wordpress.com/pinghub/wpcom/me/newest-note-data’ failed: Error during WebSocket handshake: Unexpected response code: 403

In Safari it’s slightly less specific, but the issue is the same:

WebSocket connection to ‘wss://public-api.wordpress.com/pinghub/wpcom/me/newest-note-data’ failed: Unexpected response code: 403

This issue has been reported various places online but nowhere do I find a true fix.

As indicated in some of those support threads I linked to above, it’s due to an issue with the Notifications module in Jetpack. What? It’s something I was not even really aware of and that can be extremely easy to ignore, especially if your site is not blog-focused or does not have active commenting. Turns out Jetpack has this little “Notifications” icon up in the admin bar next to your Gravatar, where you can get the latest updates on what’s happening on your blog.

If you’re like me, you don’t need or want the Notifications tool. And if getting rid of it gets rid of that stupid error that’s constantly popping up in the console, all the better.

Side note: Did you know there’s a hidden page where you can access the settings for a ton of Jetpack modules? (Neither did I.) Just go to /wp-admin/admin.php?page=jetpack_modules in your WP admin and there it is! If you’re just dealing with this issue on one particular site, and don’t want to write any code, simply deactivate the Notifications module here and you’re done. If this is something you are dealing with on many sites though and you want an automatic, code-based solution to stick into your base theme, read on…

I tried using Jetpack’s instructions for disabling a module via code. (Fun fact, the name for this module in the code is not 'notifications' as you might expect; it’s 'notes'.) Frustratingly, neither the 'option_jetpack_active_modules' filter nor the 'jetpack_get_available_modules' filter worked. I even scoured the Jetpack source code for other filters I might need to use, and none of them made a difference.

Oh, I was able to get 'notes' not to appear in the array those filters returns, and I was even able to use that to get “Notifications” to always appear as “inactive” on that hidden settings page.

But the error kept showing up in the console, and the little Notifications icon was still in the admin bar. The module was not inactive.

Finally, through further source code poking, I discovered there’s a static public method called Jetpack::deactivate_module(). I tried running it in those aforementioned filters but it resulted in an HTTP 500 error. (Strangely, it was an out-of-memory error. Shrug.)

So I decided to try the method in different actions. After some trial and error, it seems the 'init' action works. But that’s a bit of overkill, running this code on every front end page load, especially since once the module is deactivated, it should stay deactivated, so my solution is to run it on the 'admin_init' action instead. Especially since this issue only happens for logged-in users anyway, it should be adequate.

Here’s a bit of code you can stick into your theme’s functions.php file or wherever you like to put things like this:

add_action('admin_init', 'rm34_jetpack_deactivate_modules');
function rm34_jetpack_deactivate_modules() {
    if (class_exists('Jetpack') && Jetpack::is_module_active('notes')) {
        Jetpack::deactivate_module('notes');
    }
}

A WordPress URL rewrite rule to phase out year/month folders in the Media Library

This is one of those fixes where it is probably worth me explaining the very specific use case I needed it for first, to serve as an example of exactly why anyone would need this, because on the surface it may seem pointless.

Picture this: Restaurant client. Table cards with QR codes linking directly to menu PDFs, so customers can look at the menu on their phones instead of using a physical print menu that has been handled by dozens of other people.

Note to the future: I’m not sure what the restaurant experience looks like in your world. What I’m describing may be ubiquitous for you now, or may be a complete head-scratcher. Assuming it’s safe for you to touch your head. For context, I am writing this in the midst of the 2020 COVID-19 pandemic.

Here’s the problem: Menus change. URLs referenced by a QR code do not. By default, WordPress automatically creates year and month subfolders inside wp-content/uploads and puts files in the folder for the year and month the post they’re attached to was created, or if you’re uploading directly into the Media Library, not attached to a post, then the year and month the file was uploaded.

So that means that the URLs embedded in my client’s QR codes contain 2020/09. But now it’s October, so if they upload a replacement file today, its URL will contain 2020/10 and the QR code will not work. I should note at this point that I do not like the default WordPress behavior of putting files into these subfolders, but I sometimes forget to turn off this setting when I’m creating a new site, or — as is the case here — I’m working on a site someone else originally set up.

My solution: Turn off year/month folders, so that any newly uploaded PDFs with the same filename will have the same URL. (Assuming the client deletes the old one first!)

You may be thinking, well, that’s great, if you had done this before the QR codes were created. Yes, exactly. That’s where this rewrite rule comes in.

When you turn off the year/month folder setting, it doesn’t move any existing files or change any code that links to them. This purely affects new uploads going forward. So what I need is a rewrite rule that will allow existing file URLs with the year/month path to continue working, while automatically removing that bit from the URL and trying to find the same file in the main uploads folder, if there’s no file at the year/month URL.

OK, here’s the code:

# Redirect file URLs from year/month subfolders to base uploads folder if not found
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /wp-content/uploads/
RewriteRule ^index\.php$ – [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([0-9]{4})/([0-9]{2})/(.*) /wp-content/uploads/$3 [L,R=301]
</IfModule>

This should go not in your main .htaccess file, but in an .htaccess placed inside your wp-content/uploads folder.

Let’s assess what’s going on here, line by line.

# Redirect file URLs from year/month subfolders to base uploads folder if not found

Just a comment so we remember what this is all about. You may think you’ll remember. But you probably won’t. Comments are your future friend.

<IfModule mod_rewrite.c> and </IfModule>

Apache configuration conditionals wrapper for all of our actions, to make sure this code doesn’t run if mod_rewrite isn’t enabled. Honestly I often leave this out because… come on, the entire site is going to be broken if mod_rewrite isn’t enabled.

RewriteEngine On

If you don’t know what this is about, RTFM. (The “F” is directed at the manual, not you. I hate the Apache documentation.)

RewriteBase /wp-content/uploads/

This is the reference point for the ^ later on. Needs to be the relative path of the uploads folder below your WordPress site’s base URL.

RewriteRule ^index\.php$ - [L]

Honestly we probably don’t need this line, as there shouldn’t be any index.php files inside your uploads folder anyway, but it just feels weird not to include it. This just says “don’t do any rewrites to the index.php file.”

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

These lines are very common in this type of rewrite instruction set, and in fact come straight from the default WordPress rewrite rules. They are saying, only apply the following rules to URLs that don’t match existing real files or directories under this path. This is critical to keep all of your existing Media Library URLs working.

RewriteRule ^([0-9]{4})/([0-9]{2})/(.*) /wp-content/uploads/$3 [L,R=301]

This is what we’re here for. Note we are using the magic of regular expressions to make this work. There are three parenthetical groupings, though we technically could eliminate the first two sets of parentheses and change $3 to $1, but I just like having the parentheses to help me keep things straight.

([0-9]{4}) is matching a 4-digit number, representing the year folder.

([0-9]{2}) is matching a 2-digit number, representing the month folder.

And (.*) is matching… anything, representing the filename. That’s the bit we want to reference in the replacement string, /wp-content/uploads/$3 which tells Apache to serve up the filename from the year/month URL directly out of the uploads folder itself.

The final bit, if you’re not familiar with rewrite rules, [L,R=301], just says this is the last rule the previous set of conditions applies to, and that it should return an HTTP 301 (permanent redirect) status along with the redirect, which is good SEO karma.

A quick fix for the impossibility of building new menus on a WordPress site with a large number of pages

I’m working on a theme migration for a WordPress site that as a lot of pages. A lot of pages. 451 pages.

The old version of the site was using page menus, not custom menus, but it was relying on a constellation of abandoned plugins, and that approach just won’t work for the new theme. We need to build custom menus.

The problem is, building a new menu on a WordPress site that has 451 pages is a daunting task. Granted, only 39 of these pages need to go into the main navigation. (A lot of them really should probably be deleted, but that’s the client’s call, not mine.) So, I’m glad I don’t have to wrangle over 400 pages into a menu, but even 39 can be difficult given the shoddy interface WordPress provides for finding pages to add to your menu.

There are three tabs: Most Recent, View All, and Search. Most Recent is useless unless you’re just adding recently created pages to an existing menu. View All is useless because it’s paginated, inside this tiny box, and it lists the pages in an entirely inscrutable order. (OK, it’s not inscrutable. I can scrute it. But just because I understand the logic of how they’re ordered doesn’t mean that order is easy to work with.) And lastly we have Search which seems like the saving grace. But it’s actually the most maddening of the three because of two things:

  1. WordPress search sucks. It doesn’t give extra weight to page titles; it searches the full content. So even if I type in the exact title of the page I want, it’s usually not first in the list. And that’s a problem because…

  2. It only returns 10 results. That’s it. Ten. No lazy loading of more, no pagination, nothing. If your page doesn’t come back in the top 10 results, it may as well not exist.

I’d really love to rebuild the internal WordPress search engine to be smarter about weighting titles. Well, OK, no I wouldn’t. That would be a project I would not enjoy. But I would like for it to be done by someone.

Since that’s not likely to happen, at least there’s a way we can modify the search results to change the number of pages returned. I found the solution, as I often do, on StackExchange. But I dislike a few of the answer’s coding conventions, and I wanted to make one specific change for myself, so here’s my version. (You may not like using closures, especially in a scenario like this because it prevents you, or anyone else, from being able to remove this logic elsewhere. If I were writing a public plugin for this, I’d definitely make it a named function, but this should be fine for a custom theme.)

add_action('pre_get_posts', function($query) {
  if (is_admin()) {
    if (isset($_POST['action']) && $_POST['action'] == 'menu-quick-search' && isset($_POST['menu-settings-column-nonce'])) {
      if (is_a($query->query_vars['walker'], 'Walker_Nav_Menu_Checklist')) {
        $query->query_vars['posts_per_page'] = -1;
      }
    }
  }
  return $query;
}, 10, 2);

The important difference between the StackExchange sample and my code is that I changed the results from 30 to -1 which, in the WordPress universe, equals . Fun!

The standard warning is that setting posts_per_page to -1 is inherently risky because it could cause performance problems. But in my testing of this change, it does not appear to be an issue on this site with 451 pages, so I’m guessing it won’t be for you, either.

Now, instead of getting back a paltry ten results, you’ll get all the results that match your search. And the exact page title you typed in should be in there, somewhere.

Add arbitrary product data to order items in WooCommerce

This seems to be way more convoluted than it needs to be, but I’m not sure how much of that is that it’s actually convoluted, how much is that Woo’s documentation sucks, and how much is that everyone else’s tutorial on it is tl;dr.

Anyway… I just wanted to do something fairly simple. I want to have each product’s short description get sent into the order data. This is a specific use case with a client who’s syncing data over the REST API with an external system, and we’re shoehorning data into the short description that maybe could go somewhere else. The point is, use your imagination as to how this might be useful to you.

I’m stripping out a lot of the other details. All I want is a way to a) add the data to the item in the cart, and b) carry that data over into the order item meta data in the database. You may need or want more, but this will get you started.

// Add custom order item meta data to cart
add_filter('woocommerce_add_cart_item_data', function($cart_item, $product_id) {
  if (!isset($cart_item['short_description'])) {
    if ($product = wc_get_product($product_id)) {
      $cart_item['short_description'] = $product->get_short_description();
    }
  }
  return $cart_item;
}, 10, 2);

// Add custom order item data from the cart into the order
add_action('woocommerce_checkout_create_order_line_item', function($item, $cart_item_key, $values, $order) {
  if  (isset($values['short_description'])) {
    $item->add_meta_data('Short Description', $values['short_description'], true);
  }
}, 10, 4);

This is a major distillation of stuff I found in these two tutorials: How to Add a Customizable Field to a WooCommerce Product and Add Custom Cart Item Data in WooCommerce.

I spent 5 hours troubleshooting this WordPress problem so you don’t have to (starring: WooCommerce Action Scheduler)

Sorry for that “clickbaity” headline. I added the parenthetical so it might be at least marginally useful. Since my WordPress-related posts are always about how I solved a particularly weird or obscure WP issue, I usually consider their titles carefully. “What would I have googled to find a solution to this problem?” But honestly, I spent 5 hours on this yesterday partly because I wasn’t sure what to google. (And I use lowercase “google” as a generic for “conduct an Internet search”; I normally use DuckDuckGo.)

OK, so here’s the situation. This particular site is — among my normally very-low-traffic clients — one of the busiest I work on. It’s a WooCommerce site with hundreds of products and 20+ daily orders. (Yeah, 20+ orders a day is not huge, but on the scale I normally deal with, it’s a lot.)

This site runs on its own virtual private server, with 8 GB RAM and 4 vCPUs. Pretty substantial for a single site. And yet, for weeks it has been maxing out RAM and CPU resources. Not to the point where the site was in crisis mode that demanded my immediate attention, but it was frustratingly slow. Just slightly below the threshold of me dropping work on other new projects to try to fix this. (At this point I feel obliged to note that I did not actually build or maintain this site for its first couple of years of existence, so I don’t know its inner workings as well as I normally would. I just know it’s running way too many plugins and desperately needs some TLC I have not had time to give it.)

Yesterday things finally got to the breaking point. For me, at least. The client had contacted me about an unrelated issue, but as I was dealing with that, I got frustrated by seeing all of this inexplicable resource usage, so I had to address it.

As it happens, this post is actually a bit of a sequel to my last post, about getting Apache’s mod_status and mod_rewrite to play nicely on a WordPress site. About three weeks ago I finally got mod_status working on this site, and had planned to come back, when I had a chance, to investigate this issue.

If you are not familiar with mod_status, you should check it out. Apache is generally a bit of a “black box” but this lets you see exactly what’s happening with each thread — the requested URL, the requesting IP address, connection time, resource usage, etc.

I noticed an absurd number of threads were coming from the localhost and were requesting wp-admin/admin-ajax.php with a query string referencing WooCommerce’s Action Scheduler. But what to do with that information?

I’ll admit, this is where I wasted a bunch of time in fruitless searches, because I don’t know a lot about Action Scheduler. I read a few threads on the WordPress support forums and StackOverflow that kind of danced around the problem I was having but never really got at it.

Eventually I ended up in phpMyAdmin, scrutinizing the wp_actionscheduler_actions table, and trying to figure out where all of the wc_facebook_regenerate_feed actions were coming from. I used my old favorite Search-Replace-DB to try to find any instances of “facebook” in the database. (This was an utter failure, for reasons I can’t explain. But that failure was critical to why this took me so long to resolve.)

I went to Tools > Scheduled Actions in WP admin and discovered there were over 200,000 actions, although there were only about 70,000 (only!) showing up in wp_actionscheduler_actions. Mystery!

I went to wp_actionscheduler_actions again, saw that those wc_facebook_regenerate_feed actions had all been scheduled weeks ago, and decided to just chuck out the lot. I truncated the table, but within seconds it started filling up again with hundreds of wc_facebook_regenerate_feed actions, with the same weeks-old scheduled dates. Where were they coming from???

What was especially maddening to me about all of this was that I had already, weeks ago, determined that the plugin that had created these — Facebook for WooCommerce — had been causing some kind of trouble, and I had deactivated it. Yesterday I even went so far as to delete it. I scoured the theme code for references to Facebook. I looked in the file system for stray files that might be responsible. And as I mentioned before, I tried to search the database for any references to Facebook. I was getting nowhere.

Eventually I realized Search-Replace-DB was having problems, so I dove into phpMyAdmin directly and started searching individual fields, in individual tables, for “Facebook”. And that’s where I finally figured it out.

WordPress puts everything in wp_posts, and that’s a problem.

I’ve complained over the years about the database architecture in WordPress. Having built multiple custom CMSes in the years prior to when I finally, fully embraced WordPress in 2014, I have a fair bit of experience designing databases. And two things I learned in that experience were: 1) clearly define what your data tables are for, and 2) indexes make databases efficient. WordPress is awesome for many things, but it has far, far outgrown its original conception as blogging software. Custom Post Types and Custom Fields make it super-flexible, but shoving everything they create into wp_posts and wp_postmeta can create a disastrous situation.

Case in point, WooCommerce scheduled actions. In earlier iterations, those were custom posts! (As are, still, WooCommerce orders, which is totally f***ed up, if you ask me.) At some point Woo or Automattic realized scheduled actions don’t belong in wp_posts, so they created four new tables specifically for managing them. Plugins that use scheduled actions had to create new scheduled actions for migrating the old wp_posts scheduled actions into the new tables.

And that’s where I found myself. Through some curious set of circumstances with this particular site, which probably at some point included someone other than me disabling WP-Cron to try to fix some other problem, 200,000+ scheduled actions from the Facebook for WooCommerce plugin (in the wp_posts table) got queued up for migration to the new tables. And as quickly as I was deleting them from the new tables, Action Scheduler (which runs once a minute!!!) was dutifully refilling them.

(And obviously they were never actually running… perhaps because I had deactivated the plugin? Or because they were simply timing out? Who knows? But here’s something I see as a flaw with Action Scheduler: it should check to see if the plugin that scheduled an action is currently active, and if not, purge the action immediately.)

At last here was the fix. I had to run this SQL query in phpMyAdmin. (Proceed with caution! Don’t just use this code… look in your database for exactly what is causing problems and adjust accordingly.)

DELETE FROM `wp_posts` WHERE `post_title` = 'wc_facebook_regenerate_feed';

Note: I’m doing this from memory — and a glance back at my browser history from yesterday. I didn’t keep notes on exactly what the title was.

For a more generalized — and drastic — approach, you could also do this:

DELETE FROM `wp_posts` WHERE `post_type` = 'scheduled-action';

I’ll just conclude here with a nice little graph of the site’s CPU and RAM usage over the past 24 hours. It was 6 PM when I finally figured this out!