A handy note for future-me or any other WordPress plugin developers:

If you need a plugin admin link that performs a download (e.g. a CSV file), you will need to use a hook that is called earlier in the process, before WP sends HTTP headers/content to the browser.

With a typical plugin admin page that displays content, I would use:

add_action( 'admin_menu', 'foo_add_menu' );

Then in function foo_add_menu(), I would use add_menu_page like:

// my example is OO, so $this points to the object and calls the index() method
add_menu_page(
    'Plugin Page Title', 
    'Plugin Menu Title', 
    'edit_posts', 
    $this->plugin_name . '_index', 
    [$this, 'index'], 
    ''
);

If you try to do this with a method that starts a download, you will get errors like “Cannot modify header information.” This StackOverflow answer pointed me in the direction of the load-page hook to solve that.

That documentation is a bit slim, but what I figured out was add_menu_page() returns a hook name. You can take that hook name, prefix “load-”, and use the resulting string as the hook name in an add_action().

In my example above, let’s say the plugin name is “foo_plugin” and replace “index” with “download”. The resulting hook name would be:

load-admin_page_foo_plugin_download

Then I can use:

add_action( 'load-admin_page_foo_plugin_download', 'download' );

(Be careful with the hyphen vs underscores in that)

Finally, within the download() class method, I can safely modify HTTP headers to start a download:

header('Content-type: text/csv; charset=utf8');
// other headers and content...

Responses

Joe Crawford Joe Crawford mentioned this –