I need to hook after file uploaded to server, get the file path, and then prevent WordPress from saving the attachment post.

I found this filter add_filter('attachment_fields_to_save', 'attachment_stuff'); but this is after the attachment post was created, I want to hook before the post save.

Update 26.03.2018

I ended up using a custom media endpoint to save the files without saving a attachment post. Full example below in answer

  • 1
    You want to use WordPRess to upload all files to the disk and no data to the database? What about sftp or what is the idea?
    – birgire
    Mar 16, 2018 at 15:31
  • Yes, I'm using the media endpoint to upload and it works prefect, but I want to use the metadata and prevent the attachment post from saving
    – BenB
    Mar 16, 2018 at 15:35
  • What criteria, specifically, are you using to determine whether an attachment post should be created or not? Mar 16, 2018 at 15:56
  • virtual -1 to the question. It is like wanting to uplaod a file via ftp, but not seeing it when doing ls. Not sure what is the exact problem you are trying to solve, but this sounds like the wrong way. Mar 16, 2018 at 17:19

3 Answers 3


Based on your comment you appear to be using the REST API. There's no hook between uploading the file and creating the attachment post that could be used for this purpose in the API endpoint.

The best you can do appears to be to use the rest_insert_attachment action. It provides callback functions with the WP_Post object for the attachment post, and the WP_REST_Request object representing the request. This action is called immediately after the attachment post is created, but before any metadata or sizes are generated. So you would hook into here, check the request for whatever flag you are using to identify media that shouldn't get saved as a post, get the path, then delete the attachment post.

function wpse_297026_rest_insert_attachment( $attachment, $request, $creating ) {
    // Only handle new attachments.
    if ( ! $creating ) {

    // Check for parameter on request that tells us not to create an attachment post.
    if ( $request->get_param( 'flag' ) == true ) {
        // Get file path.
        $file = get_attached_file( $attachment->ID );

        // Do whatever you need to with the file path.

        // Delete attachment post.
        wp_delete_post( $attachment->ID );

        // Kill the request and send a 201 response.
        wp_die('', '', 201);
add_action( 'rest_insert_attachment', 'wpse_297026_rest_insert_attachment' )

I think it needs to be pointed out that if you're not creating attachments then you shouldn't be using the attachment endpoint. This is why we have to awkwardly kill the request in that code. Everything after rest_insert_attachment assumes the existence of an attachment post and most of the code for the controller for that endpoint is dedicated to creating and managing data that only makes sense for an attachment post. You should probably be creating your own endpoint for this sort of work.

  • 1
    I ended up creating a endpoint with the code from the wp-media endpoint without the insert_post part. I still think I would be nice if a hook / filter to prevent the post saving was existing.
    – BenB
    Mar 16, 2018 at 20:21

Uploading the file and creating an attachment for it is handled by the function media_handle_upload. As you can see from the source, it first uploads the file, then starts gathering metadata (including some lengthy stuff for audio files) and then calls wp_insert_attachment to create the attachment post. There is no place you can hook into.

The latter function is just a placeholder for wp_insert_post. Here you have quite a lot of hooks to filter metadata. However, there is only one condition preventing the post to be created, where it says if ( ! empty( $import_id ) ). And there's no obvious way to mess with $import_id. So, you're stuck.

Except that a little bit later in the function there is this call: do_action( 'add_attachment', $post_ID );. This fires just after an attachment post has been created. You can use this to immediately delete the post again:

add_action ('add_attachment', 'wpse297026_delete_post');
function wpse297026_delete_post ($post_ID) {
  wp_delete_post ($post_ID);

This will leave the uploaded file in its place, but WordPress will have lost track of it.


I ended up using the code from wp media endpoint in a custom endpoint without saving the post part.

Here is full example on twentyseventeen-child theme in case someone will need this.

  1. Add this to functions.php

    $Custom_Media_Uploader = new Custom_Media_Uploader();
    class Custom_Media_Uploader {
      function init() {
        add_action( 'rest_api_init', [ $this, 'register_routes' ] );
      function register_routes() {
        $version   = '1';
        $namespace = 'custom-end-point/v' . $version;
        $base      = 'media';
        register_rest_route( $namespace, '/' . $base, array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'upload_file' ],
                'permission_callback' => [ $this, 'file_upload_permissions' 
                'args'                => [],
        ) );
      function file_upload_permissions() {
        return is_user_logged_in();
      function upload_file( $request ) {
        $params = $request->get_params();
        if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array(
            ), true )
        ) {
            return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
      // Get the file via $_FILES or raw data.
        $files   = $request->get_file_params();
        $headers = $request->get_headers();
        if ( ! empty( $files ) ) {
            $file = $this->upload_from_file( $files, $headers );
        } else {
            $file = $this->upload_from_data( $request->get_body(), $headers );
        if ( is_wp_error( $file ) ) {
            return $file;
        $name       = basename( $file['file'] );
        $name_parts = pathinfo( $name );
        $name       = trim( substr( $name, 0, - ( 1 + strlen( $name_parts['extension'] ) ) ) );
        $url        = $file['url'];
        $type       = $file['type'];
        $file       = $file['file'];
        return [ 'url' => $url, 'type' => $type ];
     * Handles an upload via multipart/form-data ($_FILES).
     * @since 4.7.0
     * @param array $files Data from the `$_FILES` superglobal.
     * @param array $headers HTTP headers from the request.
     * @return array|WP_Error Data from wp_handle_upload().
      protected function upload_from_file( $files, $headers ) {
        if ( empty( $files ) ) {
            return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
    // Verify hash, if given.
        if ( ! empty( $headers['content_md5'] ) ) {
            $content_md5 = array_shift( $headers['content_md5'] );
            $expected    = trim( $content_md5 );
            $actual      = md5_file( $files['file']['tmp_name'] );
            if ( $expected !== $actual ) {
                return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
      // Pass off to WP to handle the actual upload.
        $overrides = array(
            'test_form' => false,
     // Bypasses is_uploaded_file() when running unit tests.
        if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
            $overrides['action'] = 'wp_handle_mock_upload';
        /** Include admin functions to get access to wp_handle_upload() */
        require_once ABSPATH . 'wp-admin/includes/admin.php';
        $file = wp_handle_upload( $files['file'], $overrides );
        if ( isset( $file['error'] ) ) {
            return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) );
        return $file;

  1. create a file on root of theme with the name page-upload.php

    <?php get_header(); ?>
         <div class="wrap">
         <div id="primary" class="content-area">
          <main id="main" class="site-main" role="main">
            <input type='file' onchange="uploadFile(this);"/>
            <div style="display:none" id="ajax-response">
                <div><b>File URL: </b><span id="file-url"></span></div>
                <div><b>File type: </b><span id="file-type"></span></div>
        </main><!-- #main -->
      </div><!-- #primary -->
    </div><!-- .wrap -->
    function uploadFile(input) {
        if (input.files && input.files[0]) {
            var file = input.files[0];
            var formData = new FormData();
            formData.append('file', file);
            // Fire the request.
                url: '<?php echo esc_url_raw( rest_url() ) ?>custom-end-point/v1/media',
                method: 'POST',
                processData: false,
                contentType: false,
                beforeSend: function (xhr) {
                    xhr.setRequestHeader('X-WP-Nonce', '<?php echo wp_create_nonce( 'wp_rest' ) ?>');
                data: formData
            }).success(function (response) {
            }).error(function (response) {
    <?php get_footer();
  2. Create and save a page with the title "upload" and navigate to it www.domain.com/upload, upload a file and you could see the returned URL and file type from the media endpoint after uploading a file.

Ensure you logged in and permalinks are set to post-name to allow the end point to function.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.