Title: WP_REST_Attachments_Controller::sideload_item
Published: August 20, 2026

---

# WP_REST_Attachments_Controller::sideload_item( WP_REST_Request $request ): 󠀁[WP_REST_Response](https://developer.wordpress.org/reference/classes/wp_rest_response/)󠁿|󠀁[WP_Error](https://developer.wordpress.org/reference/classes/wp_error/)󠁿

## In this article

 * [Parameters](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#parameters)
 * [Return](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#return)
 * [Source](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#source)
 * [Related](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#related)
 * [Changelog](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#changelog)

[ Back to top](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#wp--skip-link--target)

Side-loads a media file without creating a new attachment.

## 󠀁[Parameters](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#parameters)󠁿

 `$request`[WP_REST_Request](https://developer.wordpress.org/reference/classes/wp_rest_request/)
required

Full details about the request.

## 󠀁[Return](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#return)󠁿

 [WP_REST_Response](https://developer.wordpress.org/reference/classes/wp_rest_response/)
|[WP_Error](https://developer.wordpress.org/reference/classes/wp_error/) Response
object on success, [WP_Error](https://developer.wordpress.org/reference/classes/wp_error/)
object on failure.

## 󠀁[Source](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#source)󠁿

    ```php
    public function sideload_item( WP_REST_Request $request ) {
    	$attachment_id = (int) $request['id'];

    	$post = $this->get_post( $attachment_id );

    	if ( is_wp_error( $post ) ) {
    		return $post;
    	}

    	if (
    		! wp_attachment_is_image( $post ) &&
    		! wp_attachment_is( 'pdf', $post )
    	) {
    		return new WP_Error(
    			'rest_post_invalid_id',
    			__( 'Invalid post ID. Only images and PDFs can be sideloaded.' ),
    			array( 'status' => 400 )
    		);
    	}

    	/*
    	 * Sideloaded files are placed in the same directory as the attachment
    	 * they extend, because the file names produced here are later resolved
    	 * against that directory. An attachment stored outside the uploads
    	 * directory has no such directory to use, so there is nowhere the names
    	 * this would produce could resolve.
    	 */
    	$attached_file = get_attached_file( $attachment_id, true );
    	$subdir        = is_string( $attached_file ) && '' !== $attached_file
    		? $this->get_attachment_upload_subdir( $attached_file )
    		: null;

    	if ( ! is_string( $attached_file ) || '' === $attached_file || null === $subdir ) {
    		return new WP_Error(
    			'rest_sideload_attachment_not_in_uploads',
    			__( 'The attachment is not stored in the uploads directory, so a file cannot be sideloaded for it.' ),
    			array( 'status' => 403 )
    		);
    	}

    	if ( false === $request['convert_format'] ) {
    		// Prevent image conversion as that is done client-side.
    		add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
    	}

    	// Get the file via $_FILES or raw data.
    	$files   = $request->get_file_params();
    	$headers = $request->get_headers();

    	/*
    	 * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts.
    	 * See /wp-includes/functions.php.
    	 * With the following filter we can work around this safeguard.
    	 */
    	$attachment_filename = wp_basename( $attached_file );

    	$filter_filename = static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) {
    		return self::filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename );
    	};

    	add_filter( 'wp_unique_filename', $filter_filename, 10, 6 );

    	// Pin the upload to the attachment's own directory, rather than deriving
    	// it from the parent post's date as media_handle_upload() does for a
    	// brand new upload. See the note above where $subdir is resolved.
    	$filter_upload_dir = static function ( $uploads ) use ( $subdir ) {
    		if (
    			is_array( $uploads ) &&
    			isset( $uploads['basedir'], $uploads['baseurl'] ) &&
    			is_string( $uploads['basedir'] ) &&
    			is_string( $uploads['baseurl'] )
    		) {
    			$uploads['subdir'] = $subdir;
    			$uploads['path']   = $uploads['basedir'] . $subdir;
    			$uploads['url']    = $uploads['baseurl'] . $subdir;
    		}
    		return $uploads;
    	};

    	add_filter( 'upload_dir', $filter_upload_dir, 100 );

    	if ( ! empty( $files ) ) {
    		$file = $this->upload_from_file( $files, $headers );
    	} else {
    		$file = $this->upload_from_data( $request->get_body(), $headers );
    	}

    	remove_filter( 'wp_unique_filename', $filter_filename );
    	remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );
    	remove_filter( 'upload_dir', $filter_upload_dir, 100 );

    	if ( is_wp_error( $file ) ) {
    		return $file;
    	}

    	$type = $file['type'];
    	$path = $file['file'];

    	/** @var non-empty-string|non-empty-list<non-empty-string> $image_size */
    	$image_size = $request['image_size'];

    	/*
    	 * Validate raster sub-sizes before storing them. Two companion sizes
    	 * are exempt because wp_getimagesize() may not be able to read the
    	 * file at all: the 'animated_video' companion of an animated GIF is a
    	 * video (MP4/WebM), and a source-format original (e.g. a HEIC or JXL
    	 * kept next to its JPEG derivative) may be an unreadable format. Their
    	 * dimensions are neither validated nor recorded. The
    	 * 'animated_video_poster' companion is a real image, so it is still
    	 * read and rejected if unreadable; validate_image_dimensions() skips
    	 * only the registered-size constraint for it.
    	 */
    	$skip_dimension_read = self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size || 'animated_video' === $image_size;
    	$size                = false;

    	if ( ! $skip_dimension_read ) {
    		/*
    		 * Read the dimensions up front. A file whose dimensions cannot be
    		 * read is corrupted or an unsupported format and must be rejected
    		 * rather than silently stored with zero dimensions.
    		 */
    		$size = wp_getimagesize( $path );

    		if ( ! $size ) {
    			// Clean up the uploaded file.
    			wp_delete_file( $path );
    			return new WP_Error(
    				'rest_upload_invalid_image',
    				__( 'Could not read image dimensions. The file may be corrupted or an unsupported format.' ),
    				array( 'status' => 400 )
    			);
    		}

    		/*
    		 * Validate the dimensions against every size the file is being
    		 * registered under. An array $image_size shares one file among
    		 * several registered sizes, so the file has to satisfy each of
    		 * them; validating only the scalar case would let a name wrapped
    		 * in a one-element array skip the constraint entirely.
    		 */
    		foreach ( (array) $image_size as $size_name ) {
    			$validation = $this->validate_image_dimensions( $size[0], $size[1], $size_name, $attachment_id );
    			if ( is_wp_error( $validation ) ) {
    				// Clean up the uploaded file.
    				wp_delete_file( $path );
    				return $validation;
    			}
    		}
    	}

    	// Build sub-size data to return to the client.
    	// The client accumulates these and sends them all to the finalize
    	// endpoint, which writes the metadata in a single operation. This
    	// avoids the read-modify-write race that concurrent sideloads for the
    	// same attachment would otherwise hit.
    	$sub_size_data = array(
    		'image_size' => $image_size,
    	);

    	if ( is_array( $image_size ) ) {
    		/**
    		 * Multiple registered sizes share these dimensions, so a single
    		 * sideloaded file is reused for all of them. Arrays only carry
    		 * regular sub-sizes; the special keys below are always scalar
    		 * (ref. WP_REST_Attachments_Controller::get_special_image_sizes()). Those never skip
    		 * the read above, so $size already holds the dimensions.
    		 */
    		$sub_size_data['width']     = $size ? $size[0] : 0;
    		$sub_size_data['height']    = $size ? $size[1] : 0;
    		$sub_size_data['file']      = wp_basename( $path );
    		$sub_size_data['mime_type'] = $type;
    		$sub_size_data['filesize']  = wp_filesize( $path );
    	} elseif ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) {
    		/*
    		 * Source-format original (e.g. the HEIC kept next to its JPEG
    		 * derivative). Record the filename so finalize_item can store it
    		 * under the dedicated source-image meta key.
    		 */
    		$sub_size_data['file'] = wp_basename( $path );
    	} elseif ( 'animated_video' === $image_size || 'animated_video_poster' === $image_size ) {
    		/*
    		 * Converted-video companion of an animated GIF (the MP4/WebM or
    		 * its static first-frame poster). Record the filename so
    		 * finalize_item can store it under its dedicated meta key.
    		 */
    		$sub_size_data['file'] = wp_basename( $path );
    	} elseif ( 'scaled' === $image_size || 'original' === $image_size ) {
    		/*
    		 * 'scaled' and 'original' both replace the attachment's main file
    		 * with the supplied image and keep the file being replaced as
    		 * `original_image`, which is the untouched upload. A 'scaled'
    		 * image is downsized and an 'original' image has any EXIF
    		 * orientation already applied. This is the same swap WordPress
    		 * makes when it scales or rotates an image on upload; see
    		 * _wp_image_meta_replace_original().
    		 */
    		$sub_size_data['original_image'] = $attachment_filename;

    		// Validate the supplied image before updating the attached file.
    		// $size was read above: neither of these sizes skips that read.
    		$filesize = wp_filesize( $path );

    		if ( ! $size || ! $filesize ) {
    			// Clean up the uploaded file, which nothing references yet.
    			wp_delete_file( $path );
    			return new WP_Error(
    				'rest_sideload_invalid_image',
    				__( 'Unable to read the sideloaded image file.' ),
    				array( 'status' => 500 )
    			);
    		}

    		// Update the attached file to point to the supplied image.
    		// This writes to _wp_attached_file meta, not _wp_attachment_metadata.
    		if (
    			$attached_file !== $path &&
    			! update_attached_file( $attachment_id, $path )
    		) {
    			// Clean up the uploaded file, which nothing references yet.
    			wp_delete_file( $path );
    			return new WP_Error(
    				'rest_sideload_update_attached_file_failed',
    				__( 'Unable to update the attached file for this attachment.' ),
    				array( 'status' => 500 )
    			);
    		}

    		$sub_size_data['width']    = $size[0];
    		$sub_size_data['height']   = $size[1];
    		$sub_size_data['filesize'] = $filesize;
    		$sub_size_data['file']     = _wp_relative_upload_path( $path );
    	} else {
    		// As above, $size was already read for every size reaching here.
    		$sub_size_data['width']     = $size ? $size[0] : 0;
    		$sub_size_data['height']    = $size ? $size[1] : 0;
    		$sub_size_data['file']      = wp_basename( $path );
    		$sub_size_data['mime_type'] = $type;
    		$sub_size_data['filesize']  = wp_filesize( $path );
    	}

    	/*
    	 * Record the file names produced for this attachment so finalize can
    	 * confirm every stored sub-size was actually sideloaded here. The
    	 * values recorded are exactly the ones handed back to the client, so
    	 * finalize accepts a submission only when it echoes what was produced.
    	 */
    	foreach ( array( 'file', 'original_image' ) as $provenance_key ) {
    		if (
    			isset( $sub_size_data[ $provenance_key ] ) &&
    			is_string( $sub_size_data[ $provenance_key ] ) &&
    			'' !== $sub_size_data[ $provenance_key ]
    		) {
    			add_post_meta( $attachment_id, self::META_KEY_SIDELOAD_FILE_NAME, wp_slash( $sub_size_data[ $provenance_key ] ) );
    		}
    	}

    	return rest_ensure_response( $sub_size_data );
    }
    ```

[View all references](https://developer.wordpress.org/reference/files/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php/)
[View on Trac](https://core.trac.wordpress.org/browser/tags/7.1/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php#L2752)
[View on GitHub](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php#L2752-L3009)

## 󠀁[Related](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#related)󠁿

| Uses | Description | 
| [WP_REST_Attachments_Controller::get_attachment_upload_subdir()](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/get_attachment_upload_subdir/)`wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php` |

Returns the uploads subdirectory an attachment is stored in.

  | 
| [WP_REST_Attachments_Controller::filter_wp_unique_filename()](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/filter_wp_unique_filename/)`wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php` |

Filters wp_unique_filename during sideloads.

  | 
| [WP_REST_Attachments_Controller::validate_image_dimensions()](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/validate_image_dimensions/)`wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php` |

Validates that uploaded image dimensions are appropriate for the specified image size.

  | 
| [wp_filesize()](https://developer.wordpress.org/reference/functions/wp_filesize/)`wp-includes/functions.php` |

Wrapper for PHP filesize with filters and casting the result as an integer.

  | 
| [wp_getimagesize()](https://developer.wordpress.org/reference/functions/wp_getimagesize/)`wp-includes/media.php` |

Allows PHP’s getimagesize() to be debuggable when necessary.

  | 
| [WP_REST_Attachments_Controller::upload_from_file()](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/upload_from_file/)`wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php` |

Handles an upload via multipart/form-data ($_FILES).

  | 
| [WP_REST_Attachments_Controller::upload_from_data()](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/upload_from_data/)`wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php` |

Handles an upload via raw POST data.

  | 
| [wp_delete_file()](https://developer.wordpress.org/reference/functions/wp_delete_file/)`wp-includes/functions.php` |

Deletes a file.

  | 
| [wp_attachment_is()](https://developer.wordpress.org/reference/functions/wp_attachment_is/)`wp-includes/post.php` |

Verifies an attachment is of a given type.

  | 
| [wp_attachment_is_image()](https://developer.wordpress.org/reference/functions/wp_attachment_is_image/)`wp-includes/post.php` |

Determines whether an attachment is an image.

  | 
| [add_post_meta()](https://developer.wordpress.org/reference/functions/add_post_meta/)`wp-includes/post.php` |

Adds a meta field to the given post.

  | 
| [get_attached_file()](https://developer.wordpress.org/reference/functions/get_attached_file/)`wp-includes/post.php` |

Retrieves attached file path based on attachment ID.

  | 
| [update_attached_file()](https://developer.wordpress.org/reference/functions/update_attached_file/)`wp-includes/post.php` |

Updates attachment file path based on attachment ID.

  | 
| [_wp_relative_upload_path()](https://developer.wordpress.org/reference/functions/_wp_relative_upload_path/)`wp-includes/post.php` |

Returns relative path to an uploaded file.

  | 
| [rest_ensure_response()](https://developer.wordpress.org/reference/functions/rest_ensure_response/)`wp-includes/rest-api.php` |

Ensures a REST response is a response object (for consistency).

  | 
| [__()](https://developer.wordpress.org/reference/functions/__/)`wp-includes/l10n.php` |

Retrieves the translation of $text.

  | 
| [wp_basename()](https://developer.wordpress.org/reference/functions/wp_basename/)`wp-includes/formatting.php` |

i18n-friendly version of basename().

  | 
| [wp_slash()](https://developer.wordpress.org/reference/functions/wp_slash/)`wp-includes/formatting.php` |

Adds slashes to a string or recursively adds slashes to strings within an array.

  | 
| [add_filter()](https://developer.wordpress.org/reference/functions/add_filter/)`wp-includes/plugin.php` |

Adds a callback function to a filter hook.

  | 
| [remove_filter()](https://developer.wordpress.org/reference/functions/remove_filter/)`wp-includes/plugin.php` |

Removes a callback function from a filter hook.

  | 
| [is_wp_error()](https://developer.wordpress.org/reference/functions/is_wp_error/)`wp-includes/load.php` |

Checks whether the given variable is a WordPress Error.

  | 
| [WP_Error::__construct()](https://developer.wordpress.org/reference/classes/wp_error/__construct/)`wp-includes/class-wp-error.php` |

Initializes the error.

  |

[Show 17 more](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#)
[Show less](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#)

## 󠀁[Changelog](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/sideload_item/?output_format=md#changelog)󠁿

| Version | Description | 
| [7.1.0](https://developer.wordpress.org/reference/since/7.1.0/) | Introduced. |

## User Contributed Notes

You must [log in](https://login.wordpress.org/?redirect_to=https%3A%2F%2Fdeveloper.wordpress.org%2Freference%2Fclasses%2Fwp_rest_attachments_controller%2Fsideload_item%2F)
before being able to contribute a note or feedback.