WordPress 6.3 headers already sent error

No, you should not use output buffering, and instead, you just need to use the right hook to refresh the page — the one that you’ve used runs after the headers and content are already sent to the browser, hence it’s not the right hook or that it should only be used to output the content for your custom admin menu page.

The hook that you should use is load-<page hook>, where <page hook> is the return value of add_submenu_page() which is a hook suffix like my_custom_post_type_page_fs_settings in your case.

So for example:

  • In the fs_settings_admin_callback function:

    $hook_suffix = add_submenu_page(
        'edit.php?post_type=my_custom_post_type',
        your code here
    );
    
    add_action( "load-$hook_suffix", 'fs_settings_maybe_generate' );
    
  • Then do the refresh in the callback specified above:

    function fs_settings_maybe_generate() {
        if ( ! empty( $_POST['generate'] ) ) {
            // your code here
    
            header( 'Refresh: 0' );
            exit;
        }
    }
    

Another hook which you can use/try is admin_post_<action> where <action> would be the value of a hidden input named action in your form. However, we won’t be using the Refresh header, but instead, do a redirect to the form page.

  • Sample form:

    <form method="post" action="admin-post.php">
        <p>
            <input type="checkbox" name="generate" id="fs-generate" />
            <label for="fs-generate">Generate</label>
        </p>
    
        <input type="hidden" name="action" value="generate" />
        <?php wp_nonce_field( 'fs-generate' ); ?>
    
        <?php submit_button(); ?>
    </form>
    
  • Now handle the form submissions:

    add_action( 'admin_post_generate', 'fs_admin_post_generate' );
    function fs_admin_post_generate() {
        check_admin_referer( 'fs-generate' );
    
        if ( ! empty( $_POST['generate'] ) ) {
            // your code here
        }
    
        // Always redirect back.
        wp_redirect( wp_get_referer() );
        exit;
    }
    

See https://developer.wordpress.org/apis/security/ for details on adding an nonce field and other security measures.

Additionally, you should also utilize the WordPress’s HTTP API for making HTTP/remote/external requests, e.g. wp_remote_get():

function fs_settings_maybe_generate() {
    if ( ! empty( $_POST['generate'] ) ) {
        $response = wp_remote_get( 'https://www.example.com/api/fs/v1/endpoint' );
        $body = wp_remote_retrieve_body( $response );
        // Do something with $body, if you want to.

        header( 'Refresh: 0' );
        exit;
    }
}