diff --git a/README.txt b/README.txt
index 3948da2..c556d6b 100644
--- a/README.txt
+++ b/README.txt
@@ -194,6 +194,16 @@ Default:     FALSE
 Description: Flag to stop feeds from using its cURL for http requests. See
              http_request_use_curl().
 
+Name:        feeds_http_file_cache_dir
+Default:     private://feeds/cache
+Description: The location on the file system where results of HTTP requests are
+             cached.
+
+Name:        feeds_sync_cache_feeds_http_interval
+Default:     21600
+Description: How often the feeds cache directory should be checked for orphaned
+             cache files.
+
 Name:        feeds_use_mbstring
 Default:     TRUE
 Description: The extension mbstring is used to convert encodings during parsing.
diff --git a/feeds.info b/feeds.info
index e9ef158..a732790 100644
--- a/feeds.info
+++ b/feeds.info
@@ -14,6 +14,8 @@ test_dependencies[] = rules:rules
 test_dependencies[] = variable:variable
 
 files[] = includes/FeedsConfigurable.inc
+files[] = includes/FeedsHTTPCache.inc
+files[] = includes/FeedsHTTPCacheItem.inc
 files[] = includes/FeedsImporter.inc
 files[] = includes/FeedsSource.inc
 files[] = libraries/ParserCSV.inc
@@ -74,6 +76,7 @@ files[] = tests/feeds_mapper_summary.test
 files[] = tests/feeds_mapper_taxonomy.test
 files[] = tests/feeds_tokens.test
 files[] = tests/FeedsEnclosureTest.test
+files[] = tests/FeedsHTTPCacheTest.test
 files[] = tests/http_request.test
 files[] = tests/parser_csv.test
 
diff --git a/feeds.install b/feeds.install
index ab26957..84e9764 100644
--- a/feeds.install
+++ b/feeds.install
@@ -55,12 +55,23 @@ function feeds_requirements($phase) {
 }
 
 /**
-  * Implement hook_uninstall()
-  */
+ * Implements hook_install().
+ */
+function feeds_install() {
+  // Activate our custom cache handler for the HTTP cache.
+  variable_set('cache_class_cache_feeds_http', 'FeedsHTTPCache');
+}
+
+/**
+ * Implements hook_uninstall().
+ */
 function feeds_uninstall() {
   variable_del('http_request_timeout');
   variable_del('feeds_reschedule');
   variable_del('feeds_use_mbstring');
+  variable_del('feeds_http_file_cache_dir');
+  variable_del('feeds_sync_cache_feeds_http_last_check');
+  variable_del('feeds_sync_cache_feeds_http_interval');
 }
 
 /**
@@ -706,3 +717,11 @@ function feeds_update_7212() {
     db_create_table('cache_feeds_http', $schema);
   }
 }
+
+/**
+ * Set cache class for Feeds HTTP cache.
+ */
+function feeds_update_7213() {
+  // Activate our custom cache handler for the HTTP cache.
+  variable_set('cache_class_cache_feeds_http', 'FeedsHTTPCache');
+}
diff --git a/feeds.module b/feeds.module
index 689df81..04bf60d 100644
--- a/feeds.module
+++ b/feeds.module
@@ -53,19 +53,32 @@ function feeds_cron() {
     ->execute();
 
   // Find importers that need to be rescheduled.
-  if (!$importers = feeds_reschedule()) {
-    return;
-  }
-
-  // @todo Maybe we should queue this somehow as well. This could be potentially
-  // very long.
-  $sources = db_query("SELECT feed_nid, id FROM {feeds_source} WHERE id IN (:ids)", array(':ids' => $importers));
+  $importers = feeds_reschedule();
+  if ($importers) {
+    // @todo Maybe we should queue this somehow as well. This could be potentially
+    // very long.
+    $sources = db_query("SELECT feed_nid, id FROM {feeds_source} WHERE id IN (:ids)", array(':ids' => $importers));
+
+    foreach ($sources as $source) {
+      feeds_source($source->id, $source->feed_nid)->schedule();
+    }
 
-  foreach ($sources as $source) {
-    feeds_source($source->id, $source->feed_nid)->schedule();
+    feeds_reschedule(FALSE);
   }
 
-  feeds_reschedule(FALSE);
+  // Sync the files in the cache directory with entries in the cache every now
+  // and then. By default: every six hours.
+  $last_check = variable_get('feeds_sync_cache_feeds_http_last_check');
+  $interval = variable_get('feeds_sync_cache_feeds_http_interval', 21600);
+  if ($last_check < (REQUEST_TIME - $interval)) {
+    // Check first if the task isn't already queued.
+    $queue = DrupalQueue::get('feeds_sync_cache_feeds_http');
+    if ($queue->numberOfItems() < 1) {
+      // Queue sync task.
+      FeedsHTTPCache::getInstance('cache_feeds_http')->startSync();
+    }
+    variable_set('feeds_sync_cache_feeds_http_last_check', REQUEST_TIME);
+  }
 }
 
 /**
@@ -108,6 +121,9 @@ function feeds_cron_queue_info() {
   $queues['feeds_push_unsubscribe'] = array(
     'worker callback' => 'feeds_push_unsubscribe',
   );
+  $queues['feeds_sync_cache_feeds_http'] = array(
+    'worker callback' => 'feeds_sync_cache_feeds_http',
+  );
 
   return $queues;
 }
@@ -160,6 +176,14 @@ function _feeds_queue_worker_helper(array $job, $method) {
 }
 
 /**
+ * Scheduler callback for syncing the feeds cache directory with the entries in
+ * the cache.
+ */
+function feeds_sync_cache_feeds_http(array $job) {
+  FeedsHTTPCache::getInstance('cache_feeds_http')->sync($job['files']);
+}
+
+/**
  * Scheduler callback for unsubscribing from PuSH hubs.
  */
 function feeds_push_unsubscribe($job) {
diff --git a/includes/FeedsHTTPCache.inc b/includes/FeedsHTTPCache.inc
new file mode 100644
index 0000000..944a2d1
--- /dev/null
+++ b/includes/FeedsHTTPCache.inc
@@ -0,0 +1,310 @@
+<?php
+
+/**
+ * Cache implementation for the Feeds HTTP cache.
+ */
+class FeedsHTTPCache extends DrupalDatabaseCache {
+
+  /**
+   * Returns cache object.
+   *
+   * @param string $bin
+   *   The cache bin.
+   *
+   * @return FeedsHTTPCache
+   *   An instance of FeedsHTTPCache.
+   */
+  public static function getInstance($bin) {
+    $cache_object = _cache_get_object($bin);
+    if (!($cache_object instanceof self)) {
+      // A different cache class could be used for the cache_feeds_http bin that
+      // does not extend FeedsHTTPCache. In this case, just instantiate the
+      // FeedsHTTPCache class.
+      $cache_object = new self($bin);
+    }
+    return $cache_object;
+  }
+
+  /**
+   * Returns cache dir.
+   *
+   * @return string
+   *   The cache dir to use.
+   */
+  public function getCacheDir() {
+    $dir = variable_get('feeds_http_file_cache_dir', NULL);
+    if ($dir) {
+      return $dir;
+    }
+    else {
+      $schemes = file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE);
+      $scheme = isset($schemes['private']) ? 'private' : 'public';
+      return $scheme . '://feeds/cache';
+    }
+  }
+
+  /**
+   * Returns a list of file names in the cache directory.
+   *
+   * @return array
+   *   A list of files.
+   */
+  public function getFileList() {
+    $files = array();
+
+    $file_cache_dir = $this->getCacheDir();
+    if (is_dir($file_cache_dir)) {
+      $dir = dir($file_cache_dir);
+      while (($entry = $dir->read()) !== FALSE) {
+        if ($entry == '.' || $entry == '..') {
+          continue;
+        }
+        $files[] = $entry;
+      }
+      $dir->close();
+    }
+
+    return $files;
+  }
+
+  /**
+   * Constructs a file path for a certain cache ID.
+   *
+   * @param string $cid
+   *   The cache ID to construct a file path for.
+   *
+   * @return string
+   *   The constructed file path.
+   */
+  public function constructFilePath($cid) {
+    return $this->getCacheDir() . '/' . $cid;
+  }
+
+  /**
+   * Saves raw contents to a file in the cache directory.
+   *
+   * @param string $cid
+   *   The cache ID.
+   * @param object $response
+   *   The HTTP Response object.
+   *
+   * @return string|null
+   *   The name of the file that was created or NULL if no file was created.
+   */
+  public function saveFile($cid, $response) {
+    if (isset($response->data)) {
+      $file_cache_dir = $this->getCacheDir();
+      file_prepare_directory($file_cache_dir, FILE_CREATE_DIRECTORY);
+      $filename = $this->constructFilePath($cid);
+      file_put_contents($filename, $response->data);
+
+      return $filename;
+    }
+  }
+
+  /**
+   * Deletes a file from the cache directory.
+   *
+   * @param string $cid
+   *   The file to delete.
+   */
+  protected function deleteFile($cid) {
+    $filename = $this->constructFilePath($cid);
+    if (is_file($filename)) {
+      drupal_unlink($filename);
+    }
+  }
+
+  /**
+   * Deletes multiple files from the cache directory.
+   *
+   * @param array $cids
+   *   The files to delete.
+   */
+  protected function deleteMultipleFiles(array $cids) {
+    foreach ($cids as $cid) {
+      $this->deleteFile($cid);
+    }
+  }
+
+  /**
+   * Deletes all files from the cache directory.
+   */
+  protected function deleteAllFiles() {
+    $file_cache_dir = $this->getCacheDir();
+    if (drupal_realpath($file_cache_dir) && file_exists($file_cache_dir)) {
+      @file_unmanaged_delete_recursive($file_cache_dir);
+    }
+  }
+
+  /**
+   * Deletes files from the cache directory starting with a certain string.
+   *
+   * @param string $string
+   *   The string with which the file name should start.
+   */
+  protected function deleteFilesStartingWith($string) {
+    $mask = '/^' . preg_quote($string) . '/';
+    $files_to_delete = file_scan_directory($this->getCacheDir(), $mask);
+    foreach ($files_to_delete as $file) {
+      file_unmanaged_delete($file->uri);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Converts data to a FeedsHTTPCacheItem object, if needed.
+   */
+  protected function prepareItem($cache) {
+    $cache = parent::prepareItem($cache);
+
+    if (isset($cache->data) && is_object($cache->data) && !($cache->data instanceof FeedsHTTPCacheItem)) {
+      // Convert to a FeedsHTTPCacheItem object.
+      $cache->data = new FeedsHTTPCacheItem($cache->cid, $cache->data);
+    }
+
+    return $cache;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clear($cid = NULL, $wildcard = FALSE) {
+    if (empty($cid)) {
+      // Clean up expired cached files.
+      $cache_lifetime = variable_get('cache_lifetime', 0);
+
+      // Check if expired cached files should be cleaned up now.
+      if ($cache_lifetime) {
+        $cache_flush = variable_get('cache_flush_' . $this->bin, 0);
+        $flush_expired = $cache_flush && REQUEST_TIME > ($cache_flush + $cache_lifetime);
+      }
+      else {
+        $flush_expired = TRUE;
+      }
+
+      if ($flush_expired) {
+        // Clear files for which the cache entries are expired.
+        $result = db_select($this->bin)
+          ->fields($this->bin, array('cid'))
+          ->condition('expire', CACHE_PERMANENT, '<>')
+          ->condition('expire', REQUEST_TIME, '<')
+          ->execute()
+          ->fetchAllAssoc('cid');
+        $cids = array_keys($result);
+
+        $this->deleteMultipleFiles($cids);
+      }
+    }
+    else {
+      if ($wildcard) {
+        if ($cid == '*') {
+          $this->deleteAllFiles();
+        }
+        else {
+          $this->deleteFilesStartingWith($cid);
+        }
+      }
+      elseif (is_array($cid)) {
+        $this->deleteMultipleFiles($cid);
+      }
+      else {
+        $this->deleteFile($cid);
+      }
+    }
+
+    parent::clear($cid, $wildcard);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($cid, $item, $expire = CACHE_PERMANENT) {
+    if ($item instanceof FeedsHTTPCacheItem) {
+      // Given item already got rid of the raw data.
+      $item->setCid($cid);
+      return parent::set($cid, $item, $expire);
+    }
+
+    // Create a cache item to only cache what is needed.
+    // The cache item will take care of saving the raw data to a file.
+    $item = new FeedsHTTPCacheItem($cid, $item);
+
+    parent::set($cid, $item, $expire);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    // Check database first.
+    if (!parent::isEmpty()) {
+      return FALSE;
+    }
+
+    // Check if the cache directory is empty.
+    $file_cache_dir = $this->getCacheDir();
+
+    if (is_dir($file_cache_dir)) {
+      $dir = dir($file_cache_dir);
+      while (($entry = $dir->read()) !== FALSE) {
+        if ($entry == '.' || $entry == '..') {
+          continue;
+        }
+        // At least one file is found, so the cache directory is not empty.
+        $dir->close();
+        return FALSE;
+      }
+      // No files found. The cache directory is empty.
+      $dir->close();
+      return TRUE;
+    }
+
+    // No cache dir found.
+    return TRUE;
+  }
+
+  /**
+   * Queues all files to be synced with the cache list.
+   */
+  public function startSync() {
+    // Get all files currently in the cache directory.
+    $files = $this->getFileList();
+
+    $queue = DrupalQueue::get('feeds_sync_cache_feeds_http');
+    $queue->createItem(array(
+      'files' => $files,
+    ));
+  }
+
+  /**
+   * Removes files for which an entry no longer appears in the cache.
+   *
+   * @param array $files
+   *   The files to check.
+   */
+  public function sync(array $files) {
+    if (count($files) > 50) {
+      // There are more than 50 files in the cache directory to check. Take the
+      // first 50 and put the rest on the queue.
+      $queue = DrupalQueue::get('feeds_sync_cache_feeds_http');
+      $queue->createItem(array(
+        'files' => array_slice($files, 50),
+      ));
+
+      // Pick the first 50.
+      $files = array_slice($files, 0, 50);
+    }
+
+    // Try to load cache entries for each file. Since the file names are passed
+    // by reference and the file names for which a cache entry is found are
+    // removed from the array, we end up with a list of file names for which NO
+    // cache entry is found.
+    cache_get_multiple($files, $this->bin);
+
+    // The files for which no cache entry is found, can be removed.
+    $this->deleteMultipleFiles($files);
+  }
+
+}
diff --git a/includes/FeedsHTTPCacheItem.inc b/includes/FeedsHTTPCacheItem.inc
new file mode 100644
index 0000000..59ba61c
--- /dev/null
+++ b/includes/FeedsHTTPCacheItem.inc
@@ -0,0 +1,158 @@
+<?php
+
+/**
+ * Class of a cached item.
+ */
+class FeedsHTTPCacheItem {
+  /**
+   * The cache bin to save the data to.
+   *
+   * @var string
+   */
+  const CACHE_BIN = 'cache_feeds_http';
+
+  /**
+   * The cache ID.
+   */
+  protected $cid;
+
+  /**
+   * Headers of the response.
+   *
+   * @var array
+   */
+  protected $headers = array();
+
+  /**
+   * Path to the cache file.
+   *
+   * @var string
+   */
+  protected $file_path;
+
+  /**
+   * FeedsHTTPCacheItem object constructor.
+   *
+   * @param string $cid
+   *   The cache ID.
+   * @param object $response
+   *   The HTTP response object.
+   */
+  public function __construct($cid, $response) {
+    $this->setCid($cid);
+
+    // Copy over other metadata from result, but not the raw data.
+    // Value is assigned by reference to save memory.
+    foreach ($response as $key => &$value) {
+      switch ($key) {
+        case 'headers':
+          $this->headers = (array) $value;
+          break;
+
+        case 'file_path':
+          $this->setFilePath($value);
+          break;
+
+        case 'data':
+          // Data should not be cached in the database, so save data to a file
+          // instead. The whole response object is passed to save memory usage.
+          // The data could potentially be huge.
+          $this->saveResponseData($response);
+          break;
+
+        default:
+          $this->$key = $response->$key;
+      }
+    }
+  }
+
+  /**
+   * Sets cache ID.
+   *
+   * @param string $cid
+   *   The cache ID.
+   */
+  public function setCid($cid) {
+    $this->cid = $cid;
+  }
+
+  /**
+   * Sets file path.
+   *
+   * @param string $file_path
+   *   The file path to set.
+   */
+  public function setFilePath($file_path) {
+    $this->file_path = $file_path;
+  }
+
+  /**
+   * Magic isset.
+   */
+  public function __isset($member) {
+    if ($member == 'data') {
+      // Data should always be cached to a file, so in that case we check if the
+      // file exist where the data should be in.
+      return (isset($this->file_path) && file_exists($this->file_path));
+    }
+
+    return isset($this->$member);
+  }
+
+  /**
+   * Magic getter.
+   */
+  public function __get($member) {
+    if ($member == 'data') {
+      // Data is cached in a file, so when that member is requested, return the
+      // file contents.
+      return $this->getFileContents();
+    }
+
+    return $this->$member;
+  }
+
+  /**
+   * Returns cache object.
+   *
+   * @return FeedsHTTPCache
+   *   An instance of FeedsHTTPCache.
+   */
+  public function getCacheObject() {
+    return FeedsHTTPCache::getInstance(self::CACHE_BIN);
+  }
+
+  /**
+   * Gets the cached file from the file system.
+   *
+   * @return string|null
+   *   The file's contents, if the file exists.
+   *   NULL otherwise.
+   */
+  public function getFileContents() {
+    if (file_exists($this->file_path)) {
+      return file_get_contents($this->file_path);
+    }
+  }
+
+  /**
+   * Writes data to a file.
+   *
+   * @param object $response
+   *   The HTTP Response object that contains the data.
+   */
+  public function saveResponseData($response) {
+    if (isset($response->data)) {
+      // Get cache object, this object knows where to save the file.
+      $filename = $this->getCacheObject()->saveFile($this->cid, $response);
+      $this->setFilePath($filename);
+    }
+  }
+
+  /**
+   * Saves item to cache.
+   */
+  public function cacheSet() {
+    cache_set($this->cid, $this, self::CACHE_BIN);
+  }
+}
diff --git a/libraries/http_request.inc b/libraries/http_request.inc
index 84a9513..fcba35d 100644
--- a/libraries/http_request.inc
+++ b/libraries/http_request.inc
@@ -116,7 +116,7 @@ function http_request_get_common_syndication($url, $settings = array()) {
 function http_request_get($url, $username = NULL, $password = NULL, $accept_invalid_cert = FALSE, $timeout = NULL) {
   // Intra-pagedownload cache, avoid to download the same content twice within
   // one page download (it's possible, compatible and parse calls).
-  static $download_cache = array();
+  $download_cache = &drupal_static(__FUNCTION__, array());
   if (isset($download_cache[$url])) {
     return $download_cache[$url];
   }
@@ -296,7 +296,7 @@ function http_request_get($url, $username = NULL, $password = NULL, $accept_inva
   // In case of 304 Not Modified try to return cached data.
   if ($result->code == 304) {
 
-    if (isset($last_result)) {
+    if (isset($last_result->data)) {
       $last_result->from_cache = TRUE;
       return $last_result;
     }
@@ -423,8 +423,9 @@ function http_request_get_cache($url) {
  * @param stdClass $result
  *   The result of the HTTP request.
  */
-function http_request_set_cache($url, stdClass $result) {
-  cache_set(hash('sha256', $url), $result, 'cache_feeds_http');
+function http_request_set_cache($url, $result) {
+  $item = ($result instanceof FeedsHTTPCacheItem) ? $result : new FeedsHTTPCacheItem(hash('sha256', $url), $result);
+  $item->cacheSet();
 }
 
 /**
diff --git a/tests/FeedsHTTPCacheTest.test b/tests/FeedsHTTPCacheTest.test
new file mode 100644
index 0000000..2577afc
--- /dev/null
+++ b/tests/FeedsHTTPCacheTest.test
@@ -0,0 +1,513 @@
+<?php
+
+/**
+ * @coversDefaultClass FeedsHTTPCache
+ * @group feeds
+ */
+class FeedsHTTPCacheTest extends FeedsWebTestCase {
+  /**
+   * Default Feeds cache dir.
+   *
+   * @var string
+   */
+  const FEEDS_CACHE_DIR = 'private://feeds/cache';
+
+  /**
+   * The cache object.
+   *
+   * @var FeedsHTTPCache
+   */
+  protected $cache;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'FeedsHTTPCache class test',
+      'description' => 'Covers class FeedsHTTPCache.',
+      'group' => 'Feeds',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Create an instance of FeedsHTTPCache to test with.
+    $this->cache = new FeedsHTTPCache('cache_feeds_http');
+  }
+
+  /**
+   * Creates a dummy HTTP response.
+   *
+   * @param bool $with_data
+   *   (optional) If data should be included.
+   *   Defaults to TRUE.
+   *
+   * @return object
+   *   The created response.
+   */
+  protected function createHttpResponse($with_data = TRUE) {
+    $response = new stdClass();
+    $response->code = 200;
+    $response->headers = array(
+      'content-type' => 'text/plain; charset=utf-8',
+      'x-host' => 'http://www.example.com',
+    );
+    if ($with_data) {
+      $response->data = static::randomString(255);
+    }
+
+    return $response;
+  }
+
+  /**
+   * Creates a cache record, bypassing the API.
+   *
+   * @param string $cid
+   *   (optional) The cache item ID.
+   * @param object $response
+   *   (optional) The response to save.
+   *
+   * @return string
+   *   The cache item ID.
+   */
+  protected function createCacheRecord($cid = NULL, $response = NULL) {
+    if (empty($cid)) {
+      $cid = static::randomName();
+    }
+    if (empty($response)) {
+      $response = $this->createHttpResponse(FALSE);
+      $response->file_path = static::FEEDS_CACHE_DIR . '/' . $cid;
+    }
+
+    $record = new stdClass();
+    $record->cid = $cid;
+    $record->data = serialize($response);
+    $record->expire = CACHE_PERMANENT;
+    $record->created = REQUEST_TIME;
+    $record->serialized = TRUE;
+
+    drupal_write_record('cache_feeds_http', $record);
+
+    return $cid;
+  }
+
+  /**
+   * Creates a cache record using the API.
+   *
+   * @param string $cid
+   *   (optional) The cache item ID.
+   * @param object $response
+   *   (optional) The response to save.
+   *
+   * @return string
+   *   The cache item ID.
+   */
+  protected function createCacheRecordUsingApi($cid = NULL, $response = NULL) {
+    if (empty($cid)) {
+      $cid = static::randomName();
+    }
+    if (empty($response)) {
+      $response = $this->createHttpResponse();
+    }
+    $this->cache->set($cid, $response);
+
+    return $cid;
+  }
+
+  /**
+   * Asserts that an item with a certain ID exists in the feeds cache table.
+   *
+   * @param string $cid
+   *   The cache item ID.
+   */
+  protected function assertCacheItemExists($cid) {
+    $message = format_string('Cache item @cid exists.', array(
+      '@cid' => $cid,
+    ));
+
+    $count = db_select('cache_feeds_http')
+      ->fields('cache_feeds_http', array('cid'))
+      ->condition('cid', $cid)
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertEqual(1, $count, $message);
+  }
+
+  /**
+   * Asserts that an item with a certain ID does not exist in the feeds cache
+   * table.
+   *
+   * @param string $cid
+   *   The cache item ID.
+   */
+  protected function assertNoCacheItemExists($cid) {
+    $message = format_string('Cache item @cid does not exist.', array(
+      '@cid' => $cid,
+    ));
+
+    $count = db_select('cache_feeds_http')
+      ->fields('cache_feeds_http', array('cid'))
+      ->condition('cid', $cid)
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertEqual(0, $count, $message);
+  }
+
+  /**
+   * Asserts that a cache file with a certain ID exists on the file system.
+   *
+   * @param string $cid
+   *   The cache item ID.
+   */
+  protected function assertCacheFileExists($cid) {
+    $message = format_string('Cache file @cid exists.', array(
+      '@cid' => $cid,
+    ));
+
+    $this->assertTrue(file_exists(static::FEEDS_CACHE_DIR . '/' . $cid), $message);
+  }
+
+  /**
+   * Asserts that a cache file with a certain ID does not exist on the file
+   * system.
+   *
+   * @param string $cid
+   *   The cache item ID.
+   */
+  protected function assertNoCacheFileExists($cid) {
+    $message = format_string('Cache file @cid does not exist.', array(
+      '@cid' => $cid,
+    ));
+
+    $this->assertFalse(file_exists(static::FEEDS_CACHE_DIR . '/' . $cid), $message);
+  }
+
+  /**
+   * @covers FeedsHTTPCache::get().
+   */
+  public function testGet() {
+    $cid = static::randomName();
+    $file_data = static::randomName();
+
+    // Save a file to the cache dir.
+    $dir = static::FEEDS_CACHE_DIR;
+    file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
+    file_put_contents(static::FEEDS_CACHE_DIR . '/' . $cid, $file_data);
+
+    // Manually create a record in cache_feeds_http table.
+    $this->createCacheRecord($cid);
+
+    // Assert that the item can be get from cache.
+    $item = $this->cache->get($cid);
+    $this->assertEqual($file_data, $item->data->getFileContents());
+  }
+
+  /**
+   * @covers FeedsHTTPCache::getMultiple().
+   */
+  public function testGetMultiple() {
+    $cid1 = static::randomName();
+    $cid2 = static::randomName();
+    $cid3 = static::randomName();
+
+    $file1 = static::randomName();
+    $file2 = static::randomName();
+    $file3 = static::randomName();
+
+    // Save a few files to the cache dir.
+    $dir = static::FEEDS_CACHE_DIR;
+    file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
+    file_put_contents(static::FEEDS_CACHE_DIR . '/' . $cid1, $file1);
+    file_put_contents(static::FEEDS_CACHE_DIR . '/' . $cid2, $file2);
+    file_put_contents(static::FEEDS_CACHE_DIR . '/' . $cid3, $file3);
+
+    // Create a few records in cache_feeds_http table.
+    $this->createCacheRecord($cid1);
+    $this->createCacheRecord($cid2);
+    $this->createCacheRecord($cid3);
+
+    $cids = array(
+      $cid2,
+      $cid3,
+    );
+
+    // Assert that the expected items are get from the cache.
+    $items = $this->cache->getMultiple($cids);
+    $this->assertFalse(isset($items[$cid1]));
+    $this->assertTrue(isset($items[$cid2]));
+    $this->assertTrue(isset($items[$cid3]));
+    $this->assertEqual($file2, $items[$cid2]->data->getFileContents());
+    $this->assertEqual($file3, $items[$cid3]->data->getFileContents());
+  }
+
+  /**
+   * @covers FeedsHTTPCache::set().
+   */
+  public function testSet() {
+    $response = $this->createHttpResponse();
+    $cid = $this->createCacheRecordUsingApi(NULL, $response);
+
+    // Assert that a record was created with the expected data.
+    $record = db_select('cache_feeds_http')
+      ->fields('cache_feeds_http', array())
+      ->condition('cid', $cid)
+      ->execute()
+      ->fetch();
+
+    // Check cache record.
+    $this->assertEqual($cid, $record->cid);
+    $this->assertEqual(CACHE_PERMANENT, $record->expire);
+
+    // Check that the raw data wasn't saved in the cache record.
+    $this->assertFalse(strpos($record->data, $response->data), 'The raw data was not saved in the database.');
+
+    // Check properties.
+    $saved_response = unserialize($record->data);
+    $this->assertTrue($saved_response instanceof FeedsHTTPCacheItem, 'Cached data is instance of class FeedsHTTPCacheItem.');
+    $this->assertEqual($response->headers, $saved_response->headers);
+    $this->assertEqual(static::FEEDS_CACHE_DIR . '/' . $cid, $saved_response->file_path);
+
+    // Assert that a file was created on the file system.
+    $this->assertTrue(file_exists(static::FEEDS_CACHE_DIR . '/' . $cid));
+  }
+
+  /**
+   * @covers FeedsHTTPCache::clear().
+   *
+   * Tests if a single cached file can get cleaned up.
+   */
+  public function testClear() {
+    // Create a few cache entries.
+    $cid1 = $this->createCacheRecordUsingApi();
+    $cid2 = $this->createCacheRecordUsingApi();
+
+    // Assert that items were created in the database.
+    $this->assertCacheItemExists($cid1);
+    $this->assertCacheItemExists($cid2);
+
+    // Assert that files exist.
+    $this->assertCacheFileExists($cid1);
+    $this->assertCacheFileExists($cid2);
+
+    // Now clear first item.
+    $this->cache->clear($cid1);
+
+    // Assert that item 1 is removed from the database, but item 2 still exists.
+    $this->assertNoCacheItemExists($cid1);
+    $this->assertCacheItemExists($cid2);
+
+    // Assert that file 1 is gone as well, but file 2 still exists.
+    $this->assertNoCacheFileExists($cid1);
+    $this->assertCacheFileExists($cid2);
+  }
+
+  /**
+   * @covers FeedsHTTPCache::clear().
+   *
+   * Tests if multiple cached files can get cleaned up.
+   */
+  public function testClearMultiple() {
+    // Create a few cache entries.
+    $cid1 = $this->createCacheRecordUsingApi();
+    $cid2 = $this->createCacheRecordUsingApi();
+    $cid3 = $this->createCacheRecordUsingApi();
+    $cid4 = $this->createCacheRecordUsingApi();
+
+    // Remove item 2 and 4.
+    $this->cache->clear(array(
+      $cid2,
+      $cid4,
+    ));
+
+    // Assert that some records are removed.
+    $this->assertCacheItemExists($cid1);
+    $this->assertNoCacheItemExists($cid2);
+    $this->assertCacheItemExists($cid3);
+    $this->assertNoCacheItemExists($cid4);
+
+    // Assert that only these files were removed.
+    $this->assertCacheFileExists($cid1);
+    $this->assertNoCacheFileExists($cid2);
+    $this->assertCacheFileExists($cid3);
+    $this->assertNoCacheFileExists($cid4);
+  }
+
+  /**
+   * @covers FeedsHTTPCache::clear().
+   *
+   * Tests if expired cached files can get cleaned up.
+   */
+  public function testClearExpired() {
+    // Create a cache entry that should not expire.
+    $cid_permanent = $this->createCacheRecordUsingApi();
+
+    // Create a cache entry with an expire time in the past.
+    $cid_expire_past = static::randomName();
+    $this->cache->set($cid_expire_past, $this->createHttpResponse(), REQUEST_TIME - 60);
+
+    // Create a cache entry that expires exactly now.
+    $cid_expire_now = static::randomName();
+    $this->cache->set($cid_expire_now, $this->createHttpResponse(), REQUEST_TIME);
+
+    // Create a cache entry that expires in the future.
+    $cid_expire_future = static::randomName();
+    $this->cache->set($cid_expire_future, $this->createHttpResponse(), REQUEST_TIME + 60);
+
+    // Clear all expired cache entries.
+    $this->cache->clear();
+
+    // Assert existence of cache entries.
+    $this->assertCacheItemExists($cid_permanent);
+    $this->assertNoCacheItemExists($cid_expire_past);
+    $this->assertCacheItemExists($cid_expire_now);
+    $this->assertCacheItemExists($cid_expire_future);
+
+    // Assert existence of cache files.
+    $this->assertCacheFileExists($cid_permanent);
+    $this->assertNoCacheFileExists($cid_expire_past);
+    $this->assertCacheFileExists($cid_expire_now);
+    $this->assertCacheFileExists($cid_expire_future);
+  }
+
+  /**
+   * @covers FeedsHTTPCache::clear().
+   *
+   * Tests if expired cached files can get cleaned up using the cache_lifetime
+   * variable.
+   */
+  public function testClearExpiredUsingCacheLifeTime() {
+    variable_set('cache_lifetime', 120);
+
+    // Create a cache entry that should not expire.
+    $cid_permanent = $this->createCacheRecordUsingApi();
+
+    // Create a cache entry with an expire time in the past.
+    $cid_expire_past = static::randomName();
+    $this->cache->set($cid_expire_past, $this->createHttpResponse(), REQUEST_TIME - 60);
+
+    // Create a cache entry that expires exactly now.
+    $cid_expire_now = static::randomName();
+    $this->cache->set($cid_expire_now, $this->createHttpResponse(), REQUEST_TIME);
+
+    // Create a cache entry that expires in the future.
+    $cid_expire_future = static::randomName();
+    $this->cache->set($cid_expire_future, $this->createHttpResponse(), REQUEST_TIME + 60);
+
+    // Call clear. Nothing should be cleared because the last cache flush was
+    // too recently.
+    variable_set('cache_flush_cache_feeds_http', REQUEST_TIME - 90);
+    $this->cache->clear();
+
+    // Ensure all cached files still exist as cache lifetime has not exceeded.
+    $this->assertCacheItemExists($cid_permanent);
+    $this->assertCacheItemExists($cid_expire_past);
+    $this->assertCacheItemExists($cid_expire_now);
+    $this->assertCacheItemExists($cid_expire_future);
+    $this->assertCacheFileExists($cid_permanent);
+    $this->assertCacheFileExists($cid_expire_past);
+    $this->assertCacheFileExists($cid_expire_now);
+    $this->assertCacheFileExists($cid_expire_future);
+
+    // Now do as if cache lifetime has passed.
+    variable_set('cache_flush_cache_feeds_http', REQUEST_TIME - 121);
+    $this->cache->clear();
+
+    // Assert existence of cache entries.
+    $this->assertCacheItemExists($cid_permanent);
+    $this->assertNoCacheItemExists($cid_expire_past);
+    $this->assertCacheItemExists($cid_expire_now);
+    $this->assertCacheItemExists($cid_expire_future);
+
+    // Assert existence of cache files.
+    $this->assertCacheFileExists($cid_permanent);
+    $this->assertNoCacheFileExists($cid_expire_past);
+    $this->assertCacheFileExists($cid_expire_now);
+    $this->assertCacheFileExists($cid_expire_future);
+  }
+
+  /**
+   * @covers FeedsHTTPCache::clear().
+   *
+   * Tests if all cached files can get cleaned up.
+   */
+  public function testClearAll() {
+    // Create a few cache entries.
+    $cid1 = $this->createCacheRecordUsingApi();
+    $cid2 = $this->createCacheRecordUsingApi();
+    $cid3 = $this->createCacheRecordUsingApi();
+
+    // Now clear complete cache.
+    $this->cache->clear('*', TRUE);
+
+    // Assert that cache_feeds_http is empty.
+    $count = db_select('cache_feeds_http')
+      ->fields('cache_feeds_http', array('cid'))
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertEqual(0, $count, 'The cache_feeds_http item is empty.');
+
+    // Assert that the feeds cache dir is empty.
+    $empty = (count(glob(static::FEEDS_CACHE_DIR . '/*')) === 0);
+    $this->assertTrue($empty, 'The feeds cache directory is empty.');
+  }
+
+  /**
+   * @covers FeedsHTTPCache::clear().
+   *
+   * Tests if cached files starting with a certain string can get cleaned up.
+   */
+  public function testClearUsingWildcard() {
+    // Create a few cids, where the first few chars of cid1 and cid3 are the
+    // same and cid2 has a completely different string.
+    $cid1 = 'abc123';
+    $cid2 = 'def456';
+    $cid3 = 'abc789';
+    $this->createCacheRecordUsingApi($cid1);
+    $this->createCacheRecordUsingApi($cid2);
+    $this->createCacheRecordUsingApi($cid3);
+
+    // Clear all cache entries that start with 'abc'.
+    $this->cache->clear('abc', TRUE);
+
+    // Assert that all records starting with 'abc' are gone.
+    $this->assertNoCacheItemExists($cid1);
+    $this->assertCacheItemExists($cid2);
+    $this->assertNoCacheItemExists($cid3);
+
+    // Assert that all files in the cache dir starting with 'abc' are gone.
+    $this->assertNoCacheFileExists($cid1);
+    $this->assertCacheFileExists($cid2);
+    $this->assertNoCacheFileExists($cid3);
+  }
+
+  /**
+   * @covers FeedsHTTPCache::isEmpty().
+   */
+  public function testIsEmpty() {
+    // Add a record to the cache_feeds_http table.
+    $this->createCacheRecord();
+
+    // Assert that the cache is not empty.
+    $this->assertFalse($this->cache->isEmpty(), 'The cache is empty.');
+
+    // Truncate the table and assert that the cache class tells us that it is
+    // empty.
+    db_truncate('cache_feeds_http')->execute();
+    $this->assertTrue($this->cache->isEmpty());
+
+    // Add a file to the cache dir, without a entry in the database.
+    $dir = static::FEEDS_CACHE_DIR;
+    file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
+    file_put_contents(static::FEEDS_CACHE_DIR . '/abc123', static::randomName());
+    // And assert that the cache class reports that the cache is not empty.
+    $this->assertFalse($this->cache->isEmpty());
+  }
+}
diff --git a/tests/feeds/nodes_changes2.csv b/tests/feeds/nodes_changes2.csv
new file mode 100644
index 0000000..ad76ab6
--- /dev/null
+++ b/tests/feeds/nodes_changes2.csv
@@ -0,0 +1,8 @@
+Title,Body,published,GUID
+"Ut wisi enim ad minim veniam","CHANGED IN SOURCE",205200720,2
+"Nam liber tempor", "Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.",1151766000,1
+Typi non habent"","CHANGED IN SOURCE",1256326995,4
+"Lorem ipsum","Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.",1251936720,1
+"Investigationes demonstraverunt", "Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius.",946702800,5
+"Mirum est notare", "Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima.",1151066000,7
+"Eodem modo typi", "Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.",1201936720,8
\ No newline at end of file
diff --git a/tests/feeds_fetcher_http.test b/tests/feeds_fetcher_http.test
index 305ebf4..32618ad 100644
--- a/tests/feeds_fetcher_http.test
+++ b/tests/feeds_fetcher_http.test
@@ -18,6 +18,47 @@ class FeedsFileHTTPTestCase extends FeedsWebTestCase {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Include FeedsProcessor.inc so processor related constants are available.
+    module_load_include('inc', 'feeds', 'plugins/FeedsProcessor');
+
+    // Do not use curl as that will result into HTTP requests returning a 404.
+    variable_set('feeds_never_use_curl', TRUE);
+  }
+
+  /**
+   * Setup importer to import items from testing/feeds/nodes.csv.
+   */
+  public function setUpImporter() {
+    // Set up an importer.
+    $this->createImporterConfiguration('Node import', 'node');
+    $this->setSettings('node', NULL, array(
+      'content_type' => '',
+      'import_period' => 0,
+    ));
+    $this->setPlugin('node', 'FeedsHTTPFetcher');
+    $this->setPlugin('node', 'FeedsCSVParser');
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'update_existing' => FEEDS_UPDATE_EXISTING,
+    ));
+    $this->addMappings('node', array(
+      0 => array(
+        'source' => 'title',
+        'target' => 'title',
+        'unique' => TRUE,
+      ),
+      1 => array(
+        'source' => 'body',
+        'target' => 'body',
+      ),
+    ));
+  }
+
+  /**
    * Test the Feed URL form.
    */
   public function testFormValidation() {
@@ -44,4 +85,303 @@ class FeedsFileHTTPTestCase extends FeedsWebTestCase {
     $this->assertFieldByName('feeds[FeedsHTTPFetcher][source]', 'example.com');
   }
 
+  /**
+   * Tests if the result of a http request can be cached on the file system.
+   *
+   * When a first request is made, the data is retrieved from the given source
+   * url and cached on the file system.
+   * On the second request, send to the same source url, the HTTP header
+   * "If-Modified-Since" is set. This should result into a 304 HTTP status from
+   * the source, as the contents did not change in between. In such case, the
+   * data should be retrieved from the cache.
+   */
+  public function testHttpRequestUsingFileCache() {
+    // Include http request functions.
+    feeds_include_library('http_request.inc', 'http_request');
+
+    // First request.
+    $url = url('testing/feeds/nodes.csv', array('absolute' => TRUE));
+    $result = http_request_get($url);
+
+    // Assert that the first request was successful and was not retrieved from
+    // cache.
+    $this->assertEqual(200, $result->code, 'Request status code is 200.');
+    $this->assertTrue(empty($result->from_cache), 'Request was not retrieved from cache.');
+    // Assert that a file was created.
+    $file_url = 'private://feeds/cache/' . hash('sha256', $url);
+    $this->assertTrue(file_exists($file_url), format_string('The file @file_url exists.', array(
+      '@file_url' => $file_url,
+    )));
+
+    // Reset http_request_get() static cache.
+    drupal_static_reset('http_request_get');
+
+    // Write some garbage to the cached file to ensure that the cache is
+    // retrieved from that file.
+    $garbage = static::randomString(100);
+    file_put_contents($file_url, $garbage);
+
+    // Second request.
+    $result2 = http_request_get($url);
+
+    // Assert that the second request was successful and was retrieved from
+    // cache.
+    $this->assertEqual(200, $result2->code, 'Request status code is 200.');
+    $this->assertTrue(!empty($result2->from_cache), 'Request was retrieved from cache.');
+    $this->assertEqual($garbage, $result2->data, 'The cache came from the file cache.');
+
+    // Assert that the file is removed when caches are cleared.
+    drupal_flush_all_caches();
+    $this->assertFalse(file_exists($file_url), format_string('The file @file_url no longer exists.', array(
+      '@file_url' => $file_url,
+    )));
+  }
+
+  /**
+   * Tests if the source is refetched when the cached file is manually removed.
+   *
+   * A call to http_request_get() should always get us data.
+   */
+  public function testRefetchWhenCachedFileIsRemoved() {
+    // Include http request functions.
+    feeds_include_library('http_request.inc', 'http_request');
+
+    // First request.
+    $url = url('testing/feeds/nodes.csv', array('absolute' => TRUE));
+    $result1 = http_request_get($url);
+
+    // Assert that the first request was successful and was not retrieved from
+    // cache.
+    $this->assertEqual(200, $result1->code, 'Request status code is 200.');
+    $this->assertTrue(empty($result1->from_cache), 'Request was not retrieved from cache.');
+
+    // Reset http_request_get() static cache.
+    drupal_static_reset('http_request_get');
+
+    // Write some garbage to the cached file to ensure that the cache is
+    // retrieved from that file.
+    $file_url = 'private://feeds/cache/' . hash('sha256', $url);
+    $garbage = static::randomString(100);
+    file_put_contents($file_url, $garbage);
+
+    // Second request.
+    $result2 = http_request_get($url);
+
+    // Assert that the second request was successful and was retrieved from
+    // cache.
+    $this->assertEqual(200, $result2->code, 'Request status code is 200.');
+    $this->assertTrue(!empty($result2->from_cache), 'Request was retrieved from cache.');
+    $this->assertEqual($garbage, $result2->data, 'The cache came from the file cache.');
+
+    // Now remove the cached file.
+    drupal_unlink($file_url);
+    $this->assertFalse(file_exists($file_url), format_string('The file @file_url no longer exists.', array(
+      '@file_url' => $file_url,
+    )));
+
+    // Third request.
+    $result3 = http_request_get($url);
+
+    // Assert that the data is refetched, even though the source hasn't changed.
+    $this->assertEqual(200, $result3->code, 'Request status code is 200.');
+    $this->assertTrue(empty($result3->from_cache), 'Request was not retrieved from cache.');
+    $this->assertEqual($result1->data, $result3->data, 'Data is available on the response.');
+  }
+
+  /**
+   * Tests if the data is cached on the file system even when a different cache
+   * class is used.
+   *
+   * This can happen when using Memcache or Redis for all cache bins.
+   */
+  public function testHTTPCacheOverride() {
+    // Revert back to the default cache class.
+    variable_set('cache_class_cache_feeds_http', 'DrupalDatabaseCache');
+
+    // Import. We cannot test with a simple http_request_get() call here,
+    // because there is no way to override a cache class on a single request.
+    // @see _cache_get_object().
+    $this->setUpImporter();
+    $source_url = url('testing/feeds/nodes.csv', array('absolute' => TRUE));
+    $edit = array(
+      'feeds[FeedsHTTPFetcher][source]' => $source_url,
+    );
+    $this->drupalPost('import/node', $edit, t('Import'));
+    $this->assertText('Created 9 nodes');
+
+    // Assert that the raw data is not saved in the database.
+    $count = db_select('cache_feeds_http')
+      ->condition('data', '%Title,Body,published,GUID%', 'LIKE')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertEqual(0, $count, 'Raw source was not saved in the database.');
+
+    // Assert that a file was created.
+    $file_url = 'private://feeds/cache/' . hash('sha256', $source_url);
+    $this->assertTrue(file_exists($file_url), format_string('The file @file_url exists.', array(
+      '@file_url' => $file_url,
+    )));
+  }
+
+  /**
+   * Tests if cached files are cleaned up even when a different cache class is
+   * used.
+   */
+  public function testCachedFilesCleanupOnHTTPCacheOverride() {
+    // Revert back to the default cache class.
+    variable_set('cache_class_cache_feeds_http', 'DrupalDatabaseCache');
+
+    // Create a real cached file by performing an import.
+    $this->setUpImporter();
+    $source_url = url('testing/feeds/nodes.csv', array('absolute' => TRUE));
+    $edit = array(
+      'feeds[FeedsHTTPFetcher][source]' => $source_url,
+    );
+    $this->drupalPost('import/node', $edit, t('Import'));
+    $this->assertText('Created 9 nodes');
+    $file_url_with_cache_record = 'private://feeds/cache/' . hash('sha256', $source_url);
+
+    // Write a dummy cached file.
+    $dir = 'private://feeds/cache';
+    $file_url_no_cache_record = $dir . '/abc123';
+    file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
+    file_put_contents($file_url_no_cache_record, static::randomString());
+
+    // Trigger cleanup of orphaned cached files.
+    variable_del('feeds_sync_cache_feeds_http_last_check');
+    $this->cronRun();
+
+    // Assert that the dummy cached file has been cleaned up and the other file
+    // still exists.
+    $this->assertFalse(file_exists($file_url_no_cache_record), format_string('The file @file_url no longer exists.', array(
+      '@file_url' => $file_url_no_cache_record,
+    )));
+    $this->assertTrue(file_exists($file_url_with_cache_record), format_string('The file @file_url still exists.', array(
+      '@file_url' => $file_url_with_cache_record,
+    )));
+  }
+
+  /**
+   * Tests if the cron task for cleaning up cached files are run one at a time.
+   */
+  public function testCachedFilesCleanupQueue() {
+    $queue = DrupalQueue::get('feeds_sync_cache_feeds_http');
+
+    // First, make sure that a queue task is only ran every six hours.
+    variable_set('feeds_sync_cache_feeds_http_last_check', REQUEST_TIME);
+
+    // Run cron without executing the queue tasks.
+    feeds_cron();
+
+    // Assert that no task was created for cleaning up the files.
+    $this->assertEqual(0, $queue->numberOfItems(), 'No task was created for the feeds_sync_cache_feeds_http queue.');
+
+    // Unset last check and run cron.
+    variable_del('feeds_sync_cache_feeds_http_last_check');
+    feeds_cron();
+
+    // Assert that one task was created for cleaning up the files and that the
+    // variable for the last check was updated.
+    $this->assertEqual(1, $queue->numberOfItems(), 'One task was created for the feeds_sync_cache_feeds_http queue.');
+    $this->assertEqual(REQUEST_TIME, variable_get('feeds_sync_cache_feeds_http_last_check'));
+
+    // Unset last check and run cron again.
+    variable_del('feeds_sync_cache_feeds_http_last_check');
+    feeds_cron();
+
+    // Assert that there still is one task.
+    $this->assertEqual(1, $queue->numberOfItems(), 'One task exists for the feeds_sync_cache_feeds_http queue.');
+    $this->assertEqual(REQUEST_TIME, variable_get('feeds_sync_cache_feeds_http_last_check'));
+  }
+
+  /**
+   * Tests if a source is not refetched on a second import when the source did
+   * not change.
+   */
+  public function testSourceCaching() {
+    $this->setUpImporter();
+
+    $source_url = url('testing/feeds/nodes.csv', array('absolute' => TRUE));
+    $edit = array(
+      'feeds[FeedsHTTPFetcher][source]' => $source_url,
+    );
+    $this->drupalPost('import/node', $edit, t('Import'));
+
+    $this->assertText('Created 9 nodes');
+
+    // Ensure that the fetched content was cached in a file.
+    $file_url = 'private://feeds/cache/' . hash('sha256', $source_url);
+    $this->assertTrue(file_exists($file_url), format_string('The file @file_url exists.', array(
+      '@file_url' => $file_url,
+    )));
+
+    // Overwrite cached file, change one item.
+    $csv = file_get_contents($file_url);
+    $lines = explode("\n", $csv);
+    $lines[3] = '"Nam liber tempor","CHANGED IN CACHED FILE",1151766000,1';
+    $csv = implode("\n", $lines);
+    $this->verbose('<pre>' . $csv . '</pre>');
+    file_put_contents($file_url, $csv);
+
+    // Re-import file. Ensure that the data from the cache was used.
+    $this->drupalPost('import/node', $edit, t('Import'));
+    $this->assertText('Updated 1 node');
+
+    // Assert that node 3 had changed.
+    $node = node_load(3);
+    $this->assertEqual('Nam liber tempor', $node->title);
+    $this->assertEqual('CHANGED IN CACHED FILE', $node->body[LANGUAGE_NONE][0]['value']);
+  }
+
+  /**
+   * Tests if the source is refetched on a second import when the source
+   * changed.
+   */
+  public function testChangedSource() {
+    $this->setUpImporter();
+
+    $source_url = url('testing/feeds/nodes.csv', array('absolute' => TRUE));
+    $edit = array(
+      'feeds[FeedsHTTPFetcher][source]' => $source_url,
+    );
+    $this->drupalPost('import/node', $edit, t('Import'));
+
+    $this->assertText('Created 9 nodes');
+
+    // Ensure that the fetched content was cached in a file.
+    $file_url = 'private://feeds/cache/' . hash('sha256', $source_url);
+    $this->assertTrue(file_exists($file_url), format_string('The file @file_url exists.', array(
+      '@file_url' => $file_url,
+    )));
+
+    // Overwrite cached file, change one item.
+    $csv = file_get_contents($file_url);
+    $lines = explode("\n", $csv);
+    $lines[3] = '"Nam liber tempor","CHANGED IN CACHED FILE",1151766000,1';
+    $csv = implode("\n", $lines);
+    $this->verbose('<pre>' . $csv . '</pre>');
+    file_put_contents($file_url, $csv);
+
+    // Set flag that the source has changed.
+    variable_set('feeds_tests_nodes_changed', TRUE);
+
+    // Re-import file. Ensure that the content was refetched.
+    $this->drupalPost('import/node', $edit, t('Import'));
+    $this->assertText('Updated 2 nodes');
+
+    // Assert that node 1 and 4 changed.
+    $node = node_load(1);
+    $this->assertEqual('Ut wisi enim ad minim veniam', $node->title);
+    $this->assertEqual('CHANGED IN SOURCE', $node->body[LANGUAGE_NONE][0]['value']);
+    $node = node_load(4);
+    $this->assertEqual('Typi non habent', $node->title);
+    $this->assertEqual('CHANGED IN SOURCE', $node->body[LANGUAGE_NONE][0]['value']);
+
+    // Assert that node 3 had NOT changed.
+    $node = node_load(3);
+    $this->assertEqual('Nam liber tempor', $node->title);
+    $this->assertNotEqual('CHANGED IN CACHED FILE', $node->body[LANGUAGE_NONE][0]['value']);
+  }
+
 }
diff --git a/tests/feeds_tests.module b/tests/feeds_tests.module
index 34e4a75..5593370 100644
--- a/tests/feeds_tests.module
+++ b/tests/feeds_tests.module
@@ -34,6 +34,11 @@ function feeds_tests_menu() {
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
+  $items['testing/feeds/nodes.csv'] = array(
+    'page callback' => 'feeds_tests_nodes',
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
   return $items;
 }
 
@@ -181,6 +186,52 @@ function feeds_tests_files_empty() {
 }
 
 /**
+ * Page callback. Generates a test feed and simulates last-modified.
+ *
+ * This is used to test the following:
+ * - Ensure that the source is not refetched on a second import when the
+ *   source did not change.
+ * - Ensure that the source *is* refetched on a second import when the
+ *   source *did* change.
+ *
+ * @see FeedsFileHTTPTestCase::testHttpRequestUsingFileCache()
+ * @see FeedsFileHTTPTestCase::testSourceCaching()
+ * @see FeedsFileHTTPTestCase::testChangedSource()
+ */
+function feeds_tests_nodes() {
+  if (variable_get('feeds_tests_nodes_changed', FALSE)) {
+    $file = 'nodes_changes2.csv';
+    $last_modified = strtotime('Sun, 30 Mar 2016 10:19:55 GMT');
+  }
+  else {
+    $file = 'nodes.csv';
+    $last_modified = strtotime('Sun, 19 Nov 1978 05:00:00 GMT');
+  }
+
+  $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE;
+  $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE;
+
+  // Set header with last modified date.
+  drupal_add_http_header('Last-Modified', gmdate(DATE_RFC7231, $last_modified));
+
+  // Return 304 not modified if last modified.
+  if ($last_modified == $if_modified_since) {
+    drupal_add_http_header('Status', '304 Not Modified');
+    return;
+  }
+
+  // The following headers force validation of cache:
+  drupal_add_http_header('Expires', $last_modified);
+  drupal_add_http_header('Cache-Control', 'must-revalidate');
+  drupal_add_http_header('Content-Type', 'text/plain; charset=utf-8');
+
+  // Read actual feed from file.
+  $csv = file_get_contents(drupal_get_path('module', 'feeds') . '/tests/feeds/' . $file);
+
+  print $csv;
+}
+
+/**
  * Implements hook_feeds_processor_targets().
  */
 function feeds_tests_feeds_processor_targets($entity_type, $bundle) {
