diff --git a/README.txt b/README.txt
index 3948da2..7ebb234 100644
--- a/README.txt
+++ b/README.txt
@@ -194,6 +194,11 @@ 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_use_mbstring
 Default:     TRUE
 Description: The extension mbstring is used to convert encodings during parsing.
diff --git a/feeds.info b/feeds.info
index 4991435..d9ca981 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
@@ -73,6 +75,7 @@ files[] = tests/feeds_mapper_link.test
 files[] = tests/feeds_mapper_summary.test
 files[] = tests/feeds_mapper_taxonomy.test
 files[] = tests/feeds_tokens.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..c79bac1 100644
--- a/feeds.install
+++ b/feeds.install
@@ -55,12 +55,21 @@ 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');
 }
 
 /**
@@ -706,3 +715,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/includes/FeedsHTTPCache.inc b/includes/FeedsHTTPCache.inc
new file mode 100644
index 0000000..07ce08f
--- /dev/null
+++ b/includes/FeedsHTTPCache.inc
@@ -0,0 +1,204 @@
+<?php
+
+/**
+ * Cache implementation for the Feeds HTTP cache.
+ */
+class FeedsHTTPCache extends DrupalDatabaseCache {
+
+  /**
+   * 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';
+    }
+  }
+
+  /**
+   * 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;
+  }
+
+  /**
+   * 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->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, $data, $expire = CACHE_PERMANENT) {
+    // Create a cache item to only cache what is needed.
+    $item = new FeedsHTTPCacheItem($data);
+
+    // If the object contains the raw data, save it to a file.
+    if (isset($data->data)) {
+      $file_cache_dir = $this->getCacheDir();
+      file_prepare_directory($file_cache_dir, FILE_CREATE_DIRECTORY);
+      $filename = $this->constructFilePath($cid);
+      file_put_contents($filename, $data->data);
+
+      $item->setFilePath($filename);
+    }
+
+    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;
+  }
+}
diff --git a/includes/FeedsHTTPCacheItem.inc b/includes/FeedsHTTPCacheItem.inc
new file mode 100644
index 0000000..e460a1a
--- /dev/null
+++ b/includes/FeedsHTTPCacheItem.inc
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * Class of a cached item.
+ */
+class FeedsHTTPCacheItem {
+  /**
+   * Headers of the response.
+   *
+   * @var array
+   */
+  protected $headers = array();
+
+  /**
+   * Path to the cache file.
+   *
+   * @var string
+   */
+  protected $file_path;
+
+  /**
+   * FeedsHTTPCacheItem object constructor.
+   *
+   * @param object $result
+   *   The HTTP response object.
+   */
+  public function __construct($result) {
+    if (isset($result->headers) && is_array($result->headers)) {
+      $this->headers = $result->headers;
+    }
+    if (isset($result->file_path)) {
+      $this->setFilePath($result->file_path);
+    }
+  }
+
+  /**
+   * 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) {
+    return isset($this->$member);
+  }
+
+  /**
+   * Magic getter.
+   */
+  public function __get($member) {
+    return $this->$member;
+  }
+
+  /**
+   * Gets the cached file from the file system.
+   */
+  public function getFileContents() {
+    if (file_exists($this->file_path)) {
+      return file_get_contents($this->file_path);
+    }
+  }
+}
diff --git a/libraries/http_request.inc b/libraries/http_request.inc
index 39cd0ed..6fedc60 100644
--- a/libraries/http_request.inc
+++ b/libraries/http_request.inc
@@ -88,7 +88,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];
   }
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..a3273db 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,159 @@ 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 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.
+    $new = clone $result;
+    $garbage = static::randomString(100);
+    $new->data = $garbage;
+
+    $cache = new stdClass();
+    $cache->cid = hash('sha256', $url);
+    $cache->created = REQUEST_TIME;
+    $cache->expire = CACHE_PERMANENT;
+    $cache->data = $new;
+    file_put_contents($file_url, serialize($cache));
+
+    // 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 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.
+    $data = file_get_contents($file_url);
+    $cache = unserialize($data);
+    $csv = $cache->data->data;
+    $lines = explode("\n", $csv);
+    $lines[3] = '"Nam liber tempor","CHANGED IN CACHED FILE",1151766000,1';
+    $csv = implode("\n", $lines);
+    $cache->data->data = $csv;
+    $this->verbose('<pre>' . $csv . '</pre>');
+    file_put_contents($file_url, serialize($cache));
+
+    // 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.
+    $data = file_get_contents($file_url);
+    $cache = unserialize($data);
+    $csv = $cache->data->data;
+    $lines = explode("\n", $csv);
+    $lines[3] = '"Nam liber tempor","CHANGED IN CACHED FILE",1151766000,1';
+    $csv = implode("\n", $lines);
+    $this->verbose('<pre>' . $csv . '</pre>');
+    $cache->data->data = $csv;
+    file_put_contents($file_url, serialize($cache));
+
+    // 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) {
