Diego Viganò
Diego Viganò

Reputation: 173

WP_REST_Response to download a file

Is it possibile to return a document (a generated PDF, a CSV) using the WP_REST_Response in WordPress?

So far I've been registering a custom endpoint using register_rest_resource but if I try to return a file (e.g. using PHP fpassthru($f) or readfile($f) I get the "Headers already sent" error.

Using other words: how you would return a file using Wordpress REST APIs?

Any help is appreciated!

Thanks

Upvotes: 13

Views: 5658

Answers (3)

Philipp
Philipp

Reputation: 11391

By default, all REST responses are passed through json_encode() to return a JSON string. However, the REST server provides the WP hook rest_pre_serve_request that we can use to return binary data instead.

Code sample:

<?php
/**
 * Serves an image via the REST endpoint.
 *
 * By default, every REST response is passed through json_encode(), as the
 * typical REST response contains JSON data.
 *
 * This method hooks into the REST server to return a binary image.
 *
 * @param string $path Absolute path to the image to serve.
 * @param string $type The image mime type [png|jpg|gif]. Default is 'png'.
 *
 * @return WP_REST_Response The REST response object to serve an image.
 */
function my_serve_image( $path, $type = 'png' ) {
    $response = new WP_REST_Response;

    if ( file_exists( $path ) ) {
        // Image exists, prepare a binary-data response.
        $response->set_data( file_get_contents( $path ) );
        $response->set_headers( [
            'Content-Type'   => "image/$type",
            'Content-Length' => filesize( $path ),
        ] );

        // HERE → This filter will return our binary image!
        add_filter( 'rest_pre_serve_request', 'my_do_serve_image', 0, 2 );
    } else {
        // Return a simple "not-found" JSON response.
        $response->set_data( 'not-found' );
        $response->set_status( 404 );
    }

    return $response;
}

/**
 * Action handler that is used by `serve_image()` to serve a binary image
 * instead of a JSON string.
 *
 * @return bool Returns true, if the image was served; this will skip the
 *              default REST response logic.
 */
function my_do_serve_image( $served, $result ) {
    $is_image   = false;
    $image_data = null;

    // Check the "Content-Type" header to confirm that we really want to return
    // binary image data.
    foreach ( $result->get_headers() as $header => $value ) {
        if ( 'content-type' === strtolower( $header ) ) {
            $is_image   = 0 === strpos( $value, 'image/' );
            $image_data = $result->get_data();
            break;
        }
    }

    // Output the binary data and tell the REST server to not send any other
    // details (via "return true").
    if ( $is_image && is_string( $image_data ) ) {
        echo $image_data;

        return true;
    }

    return $served;
}

Sample usage:

<?php
// Register the REST endpoint.
register_rest_route( 'my_sample/v1', 'image', [
    'method' => 'GET',
    'callback' => 'my_rest_get_image'
] );

// Return the image data using our function above.
function my_rest_get_image() {
    return my_serve_image( 'path/to/image.jpeg', 'jpg' );
}

Upvotes: 11

Kevin Drost
Kevin Drost

Reputation: 401

You cannot use WP_REST_Response to do this. It is however possible to return something else with the rest api.

If you're absolutely sure you have the complete response ready (including headers, like Content-Disposition for downloads), you can simply exit; after generating the final response. Do note that this completely bypasses any hooks that would've been called afterwards, so use with caution.

An example with .csv

$filename = 'example-file.csv';
header("Access-Control-Expose-Headers: Content-Disposition", false);
header('Content-type: text/csv');
header("Content-Disposition: attachment; filename=\"$filename\"");

// output starts here, do not add headers from this point on.
$csv_file = fopen('php://output', 'w');

$csv_header = array(
    'column-1',
    'column-2',
    'column-3',
);

fputcsv($csv_file, $csv_header);

$data = array(
    array('a1', 'b1', 'c1'),
    array('a2', 'b2', 'c2'),
    array('a3', 'b3', 'c3'),
);

foreach ($data as $csv_data_entry) {
    fputcsv($csv_file, $csv_data_entry);
}

fclose($csv_file);

// With a non-file request, you would usually return the result.
// In this case, this would cause the "Headers already sent" errors, so an exit is required.
exit;

Upvotes: 1

Clemens Tolboom
Clemens Tolboom

Reputation: 1962

(I need this myself soon so formulating an answer which maybe incomplete)

Checking with WP Media we get on .../?rest_route=/wp/v2/media/ID a JSON API response having links for the media file(s) asked for.

Following along ie for image one of the source _url contains .../wp-content/uploads/2021/06/Screenshot-2021-06-18-at-10.25.05-150x150.png.

Following the comments (do not stream binary but link) add the file to WP Media collection or the custom endpoint could respond with a similar response linking to the generated AND stored file.

Then any JSON API compatible client can do what is needed. In this case generate a download link.

Upvotes: 0

Related Questions