WP_REST_Response to download a file
Asked Answered
W

3

13

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

Warwickshire answered 13/6, 2017 at 14:14 Comment(7)
“Return” to whom? The API returns JSON. So if your question basically is, “can I put binary data into JSON”, then the answer would be yes. Whether you should do that or under which circumstances it could make sense, would be a different question.Deering
I agree that API should return JSON to the JS callee. But, what if my API takes (for example) an ID of an order as input and should return the PDF of the invoice of that order?Gallegos
Then I would consider this a design flaw :p Large binary assets should not be passed around via such an API in the first place. Your API should return a URL for the PDF, that the client can then use to download it.Deering
Yep, but this would require two calls: the first to generate and save the PDF on the server disk, and a second one to actually download the PDF stored on disk.Gallegos
URLs don't have to point to "static" data or files.Deering
And the first thing you usually want an API to be, is fast. Cramming PDF data into the response - which could easily go into megabytes, and you will have to apply some form of encoding on top of that, f.e. base64 usually results in 4/3 of the size of the original data - is not going to achieve that, rather the contrary. You don't want have clients "hanging", waiting for an oversized API response to come in completely. Plus, this long connection is one less that is available in this time frame to serve other clients.Deering
Diego, did you find a solution to this?? A REST API doesn't always strictly return JSON. I've built many APIs that return different content types (e.g. JSON, XML, Binary) depending on their app's integration needs. I have a specific use-case myself. I have built a Wordpress plugin. The plugin's client side uses the WP-REST-API mostly returning JSON to the client. I just added a feature where my backend server (not Wordpress) generates a PDF file dynamically and returns it to the user to download the file. How else, with Wordpress, can we dynamically handle a URL with PHP and return a file?Tufa
B
10

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' );
}
Baywood answered 17/11, 2021 at 16:58 Comment(1)
If you are trying to remove the quotes around a string for a file, like for a csv, you can use this technique too (only adding this comment because my original search phrase was "remove quotes from WP_REST_Response" and it took a while to get here. Hopefully this comment will get indexed and lead people here)Bump
H
2

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;
Heredia answered 30/7, 2021 at 11:46 Comment(0)
O
0

(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.

Overhasty answered 27/7, 2021 at 15:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.