Permission problem with embedding featured media via the WordPress REST API

In a previous article I explained how to use WordPress as a headless CMS. But when using the WordPress REST API, you might encounter a problem when trying to embed the featured media in your content. This is because the media is not always publicly accessible. Sometimes in a way that is not obvious. This article will guide you through the process of making sure your featured media is publicly accessible.

The problem

When you try to embed the featured media in your api request as follows.

/wp-json/wp/v2/posts/123?_embed=featured_media

You might get a response with the following embedded media.

{
    "wp:featuredmedia": [
        {
            "code": "rest_forbidden",
            "message": "You don't have permission to do this.",
            "data": {
                "status": 403
            }
        }
    ]
}

This can happen even when the post that you are trying to access is publicly accessible. The problem is that the featured media is not always publicly accessible. Why is that?

Why is the featured media not publicly accessible, while the post is?

A media item in WordPress is just like a post. It has its own post status. But the post status is tied to the post it is attached to. So when the post is public, the media item is also public. Right? Not always.

The media item, or attachment, inherits the post status of the post it is first attached to. Even when you attach the same media item to another post, it will still have the same post status as the first post it was attached to. This is the default behavior of WordPress.

Imagine the following scenario. You have a post with a featured media item. You set the post to private. The featured media item will also be private. Now you create a new post, and attach the same media item to it. The media item will still be private. Even though the new post is public. And you get the rest_forbidden error when you try to embed the featured media with an api request.

How can we solve this?

The problem has been mentioned for quite some time already, as seen in this ticket from seven years ago. It is still not solved.

In the ticket someone mentions that the problem can be solved by using the rest_prepare_{$post_type} filter. This filter is called when the post is prepared for the rest response. In the suggested workaround it checks if a request post has a featured media item, fetches the media via php and construct the embedded media data in the filter function.

As the person that suggested the workaround mentions, this is not the best solution. It can cause some extra overhead by fetching the media item for every post that is requested. It could be better to make sure the media item is publicly accessible in the first place.

Making sure the media item is publicly accessible

We offload all our upload media to a CDN via the WP Offload Media plugin. This way all media items are publicly accessible anyway, we only need to make sure that WordPress abides by the same rules.

You can do this by leveraging the user_has_cap filter. This filter is called when a user capability is checked. We can use this filter to make sure that the media item is always publicly accessible.

add_filter('user_has_cap', function(array $allcaps, array $caps, array $args, WP_User $user) {
    // Bail out if the capability check is not for reading a post.
    if ($args[0] !== 'read_post') {
        return $allcaps;
    }
    $post_id = $args[2];
    // If the post is a media item, make sure the user has the capability to read it.
    if (get_post_type($post_id) === 'attachment') {
        $allcaps['read_post'] = true;
    }

    return $allcaps;
}, 10, 4);

This filter makes sure that the user has the capability to read a media item. This way the media item is always publicly accessible. And you will not get the rest_forbidden error when you try to embed the featured media in your api request.