• I’m struggling with (the infamous) ‘headers already sent’ error when attempting to use wp_redirect (actually wp_safe_redirect) within a plugin that I am creating from scratch.

    My use case is fairly straightforward. I am building a small application accessible from the main website that employees will be accessing to view information about their performance. Since the employees don’t have website WP logins, i am providing a simple login facility with a single login page which (a) validates the user and (b) depending on the type of role they have in the application, redirects them to one of up to three views (pages). A database table stores the username/password/role combination. Once validated, the user is to be offered the choice of entering one of a few pages to look up their information. My problem is here, in the (re)direction of the user to any of these choices.

    As i said, i’m doing this in a plugin instantiated by a shortcode on a page and i’ve initially created it as an object-oriented class and in the class constructor, i’ve got the following code:

    add_action('init', array($this, 'redirect_to_funnel'));
    function redirect_to_funnel() {
             $slug = get_post_field( 'post_name', get_post() );
             if ($slug == 'login-page' && $_SESSION['login_attempted']) {
                 wp_safe_redirect(home_url('my-options-choice-page'));
                 exit();
             }
    }
    

    This generates the dreaded ‘headers already sent’ error. I’ve tried numerous things. I’ve tried different hooks (‘init’, ‘wp_loaded’, ‘template_redirect’). I have read suggestions of putting this code into the theme’s functions.php file, which i have also tried with no success. I’ve also tried redirecting to my options page via the login form’s ‘action’ like this:

    function render_login_form() {
            <h1>Commission Tracker Login</h1>
            <div class="login-panel">
                <form action="option-routing.php" method="POST">
                    <input type="hidden" name="action" value="custom_login_redirect">
                    <label for="userid">User ID:</label>
                    <input type="text" id="userid" name="userid" placeholder="(Your UserID)" required></br>
                    <span><?php if (isset($userid_error)) echo {$userid_error}; ?></span>
                    <label for="pwd">Password:</label>
                    <input type="text" id="pwd" name="pwd" placeholder="(Your Password)" required></br>
                    <span><?php if (isset($password_error)) echo $password_error; ?> </span>
                    <span><?php if (isset($invalid_login_error)) echo $invalid_login_error; ?> </span>
                    <input type="submit" value="Submit" name="example_form_submit">
                   
            </div>
            </div>';
            return $login_form;
        }

    Same result. I’m baffled on how to do this within a plugin. Can anyone suggest a solution? Am i tying this to the wrong hook? Is my code in the wrong place? This error is baffling me to no end. Can someone help me to understand this and help me out of this maze?

Viewing 5 replies - 1 through 5 (of 5 total)
  • I think initis too early in the WordPress loading steps.

    Does it works at template_redirect?
    https://developer.wordpress.org/reference/functions/wp_redirect/#comment-2752

    Thread Starter jlanpheer

    (@jlanpheer)

    No, it doesn’t. I initially thought that ‘wp_loaded’ would do the trick, but that didn’t work, leading me to try ‘template_redirect’ and finally ‘init’. All resulted in the same error. Is the proper place to do this in the constructor of my object/class? My best guess is that perhaps my code was in the wrong place.

    I’m editing just to be clear, i thought that this statement would go in my class’ constructor:

    add_action('template_redirect', array($this, 'redirect_to_funnel'));

    The function would be properly placed in the class body. As in:

    class MyClass {
       function __construct() {
         add_action('template_redirect', array($this, 'redirect_to_funnel'));
         .......
       } // end constructor
       function redirect_to_funnel() {
             $slug = get_post_field( 'post_name', get_post() );
             if ($slug == 'login-page' && $_SESSION['login_attempted']) {
                 wp_safe_redirect(home_url('my-options-choice-page'));
                 exit();
             }
    } // end function
    } end class
    

    I’ve also tried putting all of this code (obviously not as a class) in the functions.php file, so removing the class references, the add_action that i tried looked like this:

    add_action('template_redirect', 'redirect_to_funnel');
    • This reply was modified 1 month, 1 week ago by jlanpheer. Reason: editing for clarification and to reduce back-and-forth questions

    Both init and wp_loaded are indeed too early, because if you add something like this:

    add_action('init', function () {
        echo '<pre>init:'.print_r(get_post(),true).'</pre>';
    });
    
    add_action('wp_loaded', function () {
        echo '<pre>wp_loaded:'.print_r(get_post(),true).'</pre>';
    });

    You’ll see that it prints both hook names, but no post, which is something your hook relies on.

    According to the docs, template_redirect is the right approach:

    This action hook executes just before WordPress determines which template page to load. It is a good hook to use if you need to do a redirect with full knowledge of the content that has been queried.

    https://developer.wordpress.org/reference/hooks/template_redirect/

    If I add something like this to a plugin (and only this), and I visit the homepage, it redirects as expected:

    add_action('template_redirect', function () {
        if (!is_home()) return;
        wp_safe_redirect('/test');
        exit;
    });

    The most common case I’ve seen for your error is that there may be PHP errors or warning output somewhere before your redirect. Do you have WP_DEBUG and WP_DEBUG_LOG set to true in wp-config.php, and perhaps also WP_DEBUG_DISPLAY set to false:

    define( 'WP_DEBUG', true );
    define( 'WP_DEBUG_LOG', true );
    define( 'WP_DEBUG_DISPLAY', false );

    Then check debug.log in wp-content to see if there are any warnings or errors after trying your code.

    The other thing I can think of is related to what you said above:

    As i said, i’m doing this in a plugin instantiated by a shortcode on a page and i’ve initially created it as an object-oriented class and in the class constructor, i’ve got the following code:

    Are you possibly trying to add the template_redirect action within the shortcode logic (nesting your hooks)? If so, that might already be too late for the hook to fire, because you’re already rendering a template.

    Thread Starter jlanpheer

    (@jlanpheer)

    Ryan, thank you for your thoughtful response. I believe that you hit on it in your last comment about potentially nesting hooks inside of my logic, i believe that is/was the culprit. I did some re-analysis of how to solve my problem and decided rather than forcibly directing the user, that i would instead (after a successful login) to instead present one or more links to them to choose from based on the assigned role and allow them to choose their option themselves. So, i was able to step around this problem.

    But, i do appreciate your response. Can you clarify for me where the proper place to put redirect code is in instances where you are doing this in a plugin? If your plugin is a class, is the ‘proper’ place to put your action and function code in the constructor itself?

    Thanks much for taking the time to help me understand this, i’m learning ‘the WordPress Way’ as i go here…..

    Ryan Sechrest

    (@sovereign)

    From a WordPress perspective, it doesn’t matter where in the class you put it, as long as you’re not adding a hook within another hook.

    At the end of the day, when your class gets instantiated, most of the time you’re just adding all of the hooks, which get fired where WordPress calls them.

    Whether you put them all in the constructor or in other methods, just depends on the complexity and size of the class, and how you want to organize/read it.

    From that architecture perspective, I set up my plugins as follows:

    • test-plugin
    • plugin
    • —- Plugin.php
    • autoloader.php
    • test-plugin.php

    That’s the basis. And then each file kind of looks like this:

    test-plugin.php

    <?php
    
    namespace RYSE\TestPlugin;
    
    // All your plugin header fields here
    
    if (!defined('ABSPATH')) exit;
    
    require_once 'autoloader.php';
    
    new Plugin(__FILE__);

    autoloader.php

    <?php
    
    namespace RYSE\TestPlugin;
    
    /**
     * Require plugin classes on demand.
     *
     * @author Ryan Sechrest
     * @package RYSE\TestPlugin
     */
    spl_autoload_register(function ($class) {
    
        // Remove organization and plugin name from namespace:
        // <organizationName>\<pluginName>\Foo\Bar => Foo\Bar
        $class = substr($class, strlen(__NAMESPACE__ . '\\'));
    
        // Convert namespace to path:
        // Foo\Bar => Foo/Bar
        $class = str_replace('\\', DIRECTORY_SEPARATOR, $class);
    
        // Append PHP file extension:
        // Foo/Bar => Foo/Bar.php
        $class = $class . '.php';
    
        // Prepend plugin directory:
        // Foo/Bar.php => plugin/Foo/Bar.php
        $class = 'plugin/' . $class;
    
        // Prepend current directory:
        // plugin/Foo/Bar.php => .../wp-content/plugins/<pluginName>/plugin/Foo/Bar.php
        $class = dirname(__FILE__) . '/' . $class;
    
        // If file does not exist, exit
        if (!file_exists($class)) return;
    
        // Require class: .../wp-content/plugins/<pluginName>/plugin/Foo/Bar.php
        require_once $class;
    });

    Plugin.php

    <?php
    
    namespace RYSE\TestPlugin;
    
    /**
     * Starting point for plugin.
     *
     * @author Ryan Sechrest
     * @package RYSE\TestPlugin
     */
    class Plugin extends BasePlugin
    {
        /**
         * Absolute path to plugin file.
         *
         * @var string .../wp-content/plugins/<pluginDir>/<pluginName>.php
         */
        private string $file = '';
    
        /***********************************************************/
    
        /**
         * Register plugin hooks.
         *
         * @param string $file ..../wp-content/plugins/<pluginDir>/<pluginName>.php
         */
        public function __construct(string $file)
        {
            $this->file = $file;
    
            $this->registerActivationHook();
            $this->registerDeactivationHook();
            $this->registerUninstallHook();
    
            $this->load();
        }
    
        /**
         * Load core plugin functionality.
         */
        private function load(): void
        {
            $this->addAdminNotice();
        }
    
        /***********************************************************/
    
        /**
         * Add admin notice.
         *
         * @return void
         */
        private function addAdminNotice(): void
        {
            add_action('admin_notices', [$this, '_addAdminNotice']);
        }
    
        /**
         * Hook to add admin notice.
         *
         * @return void
         */
        public function _addAdminNotice(): void
        {
            echo '<div class="notice notice-info">';
            echo '<p>Hi, I'm a notice.</p>';
            echo '</div>';
        }
    
        /***********************************************************/
    
        /**
         * Register plugin activation hook.
         *
         * @return void
         */
        private function registerActivationHook(): void
        {
            register_activation_hook($this->file, [$this, 'activate']);
        }
    
        /**
         * Run when plugin is activated.
         *
         * @return void
         */
        public static function activate(): void
        {
            // No actions to perform.
        }
    
        /*---------------------------------------------------------*/
    
        /**
         * Register plugin deactivation hook.
         *
         * @return void
         */
        private function registerDeactivationHook(): void
        {
            register_activation_hook($this->file, [$this, 'deactivate']);
        }
    
        /**
         * Run when plugin is deactivated.
         *
         * @return void
         */
        public static function deactivate(): void
        {
            // No actions to perform.
        }
    
        /*---------------------------------------------------------*/
    
        /**
         * Register plugin uninstall hook.
         *
         * @return void
         */
        private function registerUninstallHook(): void
        {
            register_uninstall_hook(
                $this->file,
                ['RYSE\TestPlugin\Plugin', 'uninstall']
            );
        }
    
        /**
         * Run when plugin is deleted (via the UI).
         *
         * @return void
         */
        public static function uninstall(): void
        {
            // No actions to perform.
        }
    }

    This is untested. I modified the above to keep it simple and focused on structure, but hopefully this gives you an idea. Then, as the plugin grows, I’ll add more classes in plugin, and instead of adding the hooks directly in Plugin.php, I’ll just instantiate other classes that will add those hooks.

    I should also note that this isn’t the ‘WordPress way’, but it’s my way, haha. It’s a model that has worked for me.

    • This reply was modified 1 month ago by Ryan Sechrest. Reason: Fix bullet points. Didn't render indented as in preview
Viewing 5 replies - 1 through 5 (of 5 total)
  • You must be logged in to reply to this topic.