diff --git a/libsoup/Makefile.am b/libsoup/Makefile.am
index 11ca32b..7685229 100644
--- a/libsoup/Makefile.am
+++ b/libsoup/Makefile.am
@@ -55,6 +55,7 @@ soup_headers =			\
 	soup-auth-domain.h	\
 	soup-auth-domain-basic.h  \
 	soup-auth-domain-digest.h \
+	soup-cache.h		\
 	soup-cookie.h		\
 	soup-cookie-jar.h	\
 	soup-cookie-jar-text.h	\
@@ -116,6 +117,7 @@ libsoup_2_4_la_SOURCES =		\
 	soup-auth-manager.c		\
 	soup-auth-manager-ntlm.h	\
 	soup-auth-manager-ntlm.c	\
+	soup-cache.c			\
 	soup-connection.h		\
 	soup-connection.c		\
 	soup-cookie.c			\
diff --git a/libsoup/soup-cache.c b/libsoup/soup-cache.c
new file mode 100644
index 0000000..c313ad2
--- /dev/null
+++ b/libsoup/soup-cache.c
@@ -0,0 +1,695 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-cache.c
+ *
+ * Copyright (C) 2009 Igalia S.L.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "soup-cache.h"
+#include "soup-date.h"
+#include "soup-headers.h"
+#include "soup-message.h"
+#include "soup-session-feature.h"
+#include "soup-uri.h"
+
+#include <gio/gio.h>
+
+static void soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
+
+typedef struct _SoupCacheEntry
+{
+	char *key;
+	char *filename;
+	guint freshness_lifetime;
+	gboolean must_revalidate;
+	guint date;
+	guint length;
+	SoupMessageHeaders *headers;
+} SoupCacheEntry;
+
+struct _SoupCachePrivate {
+	char *cache_dir;
+	GHashTable *cache;
+};
+
+enum {
+	PROP_0,
+	PROP_CACHE_DIR
+};
+
+#define SOUP_CACHE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_CACHE, SoupCachePrivate))
+
+G_DEFINE_TYPE_WITH_CODE (SoupCache, soup_cache, G_TYPE_OBJECT,
+			 G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
+						soup_cache_session_feature_init))
+
+static SoupCacheability
+get_cacheability (SoupMessage *msg)
+{
+	SoupCacheability cacheability;
+	const char *cache_control;
+
+	/* 1. The request method must be cacheable */
+        if (msg->method == SOUP_METHOD_GET)
+                cacheability = SOUP_CACHE_CACHEABLE;
+        else if (msg->method == SOUP_METHOD_HEAD ||
+                 msg->method == SOUP_METHOD_TRACE ||
+                 msg->method == SOUP_METHOD_CONNECT)
+                return SOUP_CACHE_UNCACHEABLE;
+        else
+                return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
+
+	cache_control = soup_message_headers_get (msg->response_headers, "Cache-Control");
+	if (cache_control) {
+		GHashTable *hash;
+
+		hash = soup_header_parse_param_list (cache_control);
+		/* 2. The 'no-store' cache directive does not appear in the
+		   headers */
+		if (g_hash_table_lookup (hash, "no-store")) {
+			soup_header_free_param_list (hash);
+			return SOUP_CACHE_UNCACHEABLE;
+		}
+
+		/* This does not appear in section 2.1, but I think it makes
+		   sense to check it too? */
+		if (g_hash_table_lookup (hash, "no-cache")) {
+			soup_header_free_param_list (hash);
+			return SOUP_CACHE_UNCACHEABLE;
+		}
+	}
+
+        switch (msg->status_code) {
+        case SOUP_STATUS_PARTIAL_CONTENT:
+        case SOUP_STATUS_NOT_MODIFIED:
+                /* We don't cache partial responses, but they only
+                 * invalidate cached full responses if the headers
+                 * don't match. Likewise with 304 Not Modified.
+                 */
+                cacheability = SOUP_CACHE_UNCACHEABLE;
+                break;
+
+        case SOUP_STATUS_MULTIPLE_CHOICES:
+        case SOUP_STATUS_MOVED_PERMANENTLY:
+        case SOUP_STATUS_GONE:
+                /* FIXME: cacheable unless indicated otherwise */
+                cacheability = SOUP_CACHE_UNCACHEABLE;
+                break;
+
+        case SOUP_STATUS_FOUND:
+        case SOUP_STATUS_TEMPORARY_REDIRECT:
+                /* FIXME: cacheable if explicitly indicated */
+                cacheability = SOUP_CACHE_UNCACHEABLE;
+                break;
+
+        case SOUP_STATUS_SEE_OTHER:
+        case SOUP_STATUS_FORBIDDEN:
+        case SOUP_STATUS_NOT_FOUND:
+        case SOUP_STATUS_METHOD_NOT_ALLOWED:
+                return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
+
+        default:
+                /* Any 5xx status or any 4xx status not handled above
+                 * is uncacheable but doesn't break the cache.
+                 */
+                if ((msg->status_code >= SOUP_STATUS_BAD_REQUEST &&
+                     msg->status_code <= SOUP_STATUS_FAILED_DEPENDENCY) ||
+                    msg->status_code >= SOUP_STATUS_INTERNAL_SERVER_ERROR)
+                        return SOUP_CACHE_UNCACHEABLE;
+
+                /* An unrecognized 2xx, 3xx, or 4xx response breaks
+                 * the cache.
+                 */
+                if ((msg->status_code > SOUP_STATUS_PARTIAL_CONTENT &&
+                     msg->status_code < SOUP_STATUS_MULTIPLE_CHOICES) ||
+                    (msg->status_code > SOUP_STATUS_TEMPORARY_REDIRECT &&
+                     msg->status_code < SOUP_STATUS_INTERNAL_SERVER_ERROR))
+                        return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
+                break;
+        }
+
+	return cacheability;
+}
+
+static void
+soup_cache_entry_free (SoupCacheEntry *entry)
+{
+	g_free (entry->filename);
+	g_free (entry->key);
+	soup_message_headers_free (entry->headers);
+	g_slice_free (SoupCacheEntry, entry);
+}
+
+static void
+headers_foreach (const char *name, const char *value, SoupMessageHeaders *headers)
+{
+	soup_message_headers_append (headers, name, value);
+}
+
+static guint
+soup_cache_entry_get_current_age (SoupCacheEntry *entry)
+{
+	time_t now = time (NULL);
+
+	return now - entry->date;
+}
+
+static gboolean
+soup_cache_entry_is_fresh_enough (SoupCacheEntry *entry, int min_fresh)
+{
+	return entry->freshness_lifetime > 
+		(min_fresh == -1) ? soup_cache_entry_get_current_age (entry) : min_fresh;
+}
+
+static char *
+soup_message_get_cache_key (SoupMessage *msg)
+{
+	SoupURI *uri;
+	GString *s;
+
+	uri = soup_message_get_uri (msg);
+	s = g_string_sized_new (20);
+
+	if (uri->host)
+		g_string_append (s, uri->host);
+
+	if (uri->path)
+		g_string_append (s, uri->path);
+
+	if (uri->query)
+		g_string_append_printf (s, "?%s", uri->query);
+
+	return g_string_free (s, FALSE);
+}
+
+static SoupCacheEntry *
+soup_cache_entry_new (SoupCache *cache, SoupMessage *msg)
+{
+	SoupCacheEntry *entry;
+	SoupMessageHeaders *headers;
+	GHashTable *cache_param_hash = NULL;
+	const char *cache_control;
+	const char *max_age;
+	const char *expires, *date;
+	const char *last_modified;
+	char *md5;
+
+	entry = g_slice_new (SoupCacheEntry);
+
+	/* key & filename */
+	entry->key = soup_message_get_cache_key (msg);
+	md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, entry->key, -1);
+	entry->filename = g_build_filename (cache->priv->cache_dir, md5, NULL);
+	g_free (md5);
+
+	/* Headers */
+	headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
+
+	soup_message_headers_foreach (msg->response_headers,
+				      (SoupMessageHeadersForeachFunc)headers_foreach,
+				      headers);
+	entry->headers = headers;
+
+	/* Section 2.3.1, Freshness Lifetime */
+
+	cache_control = soup_message_headers_get (entry->headers, "Cache-Control");
+	if (cache_control) {
+		cache_param_hash = soup_header_parse_param_list (cache_control);
+
+		/* Should we re-validate the entry when it goes stale */
+		entry->must_revalidate = (gboolean)g_hash_table_lookup (cache_param_hash, "must-revalidate");
+
+		/* If 'max-age' cache directive is present, use that */
+		max_age = g_hash_table_lookup (cache_param_hash, "max-age");
+		if (max_age) {
+			gint64 freshness_lifetime;
+
+			freshness_lifetime = g_ascii_strtoll (max_age, NULL, 10.0);
+			if (freshness_lifetime) {
+				entry->freshness_lifetime = (guint) MIN (freshness_lifetime, G_MAXUINT32);
+				goto out_freshness;
+			}
+		}
+	}
+
+	/* If the 'Expires' response header is present, use its value
+	   minus the value of the 'Date' response header */
+	expires = soup_message_headers_get (entry->headers, "Expires");
+	date = soup_message_headers_get (entry->headers, "Date");
+	if (expires && date) {
+		gint64 expires_int, date_int;
+
+		expires_int = g_ascii_strtoll (expires, NULL, 10.0);
+		date_int = g_ascii_strtoll (date, NULL, 10.0);
+
+		if (expires_int && date_int) {
+			entry->freshness_lifetime = (guint) MAX (expires_int - date_int, G_MAXUINT32);
+			goto out_freshness;
+		}
+	}
+
+	/* Otherwise an heuristic may be used */
+
+	last_modified = soup_message_headers_get (entry->headers, "Last-Modified");
+	if (last_modified &&
+	    (msg->status_code == SOUP_STATUS_OK ||
+	     msg->status_code == SOUP_STATUS_NON_AUTHORITATIVE ||
+	     /*msg->status_code == SOUP_STATUS_PARTIAL_CONTENT || */
+	     msg->status_code == SOUP_STATUS_MULTIPLE_CHOICES ||
+	     msg->status_code == SOUP_STATUS_MOVED_PERMANENTLY ||
+	     msg->status_code == SOUP_STATUS_GONE)) {
+		SoupDate *soup_date;
+		time_t now, last_modified_t;
+
+		soup_date = soup_date_new_from_string (last_modified);
+		last_modified_t = soup_date_to_time_t (soup_date);
+		now = time (NULL);
+
+#define HEURISTIC_FACTOR 0.1 /* From Section 2.3.1.1 */
+
+		entry->freshness_lifetime = MAX (0, (now - last_modified_t) * HEURISTIC_FACTOR);
+		soup_date_free (soup_date);
+
+		goto out_freshness;
+	}
+
+	/* If all else fails, make the entry expire immediately */
+	entry->freshness_lifetime = 0;
+
+ out_freshness:
+	if (cache_param_hash)
+		soup_header_free_param_list (cache_param_hash);
+
+	/* Section 2.3.2, Calculating Age */
+
+	if (!date)
+		date = soup_message_headers_get (entry->headers, "Date");
+
+	if (date) {
+		SoupDate *soup_date;
+		soup_date = soup_date_new_from_string (date);
+	
+		entry->date = soup_date_to_time_t (soup_date);
+		soup_date_free (soup_date);
+	} else {
+		/* Mmmm, can this happen? */
+		entry->date = time (NULL);
+	}
+
+	return entry;
+}
+
+static SoupCacheEntry *
+soup_cache_lookup_uri (SoupCache *cache, const char *uri)
+{
+	SoupCachePrivate *priv;
+	SoupCacheEntry *entry;
+
+	priv = cache->priv;
+
+	entry = g_hash_table_lookup (priv->cache, uri);
+
+	return entry;
+}
+
+static void
+msg_got_chunk_cb (SoupMessage *msg, SoupBuffer *chunk, SoupCacheEntry *entry)
+{
+	GFile *file;
+	GOutputStream *stream;
+
+	if (!chunk->data || !chunk->length) return;
+
+	file = g_file_new_for_path (entry->filename);
+	stream = (GOutputStream*)g_file_append_to (file, 0, NULL, NULL);
+	g_output_stream_write (stream, chunk->data, chunk->length, NULL, NULL);
+	entry->length += chunk->length;
+	g_output_stream_close (stream, NULL, NULL);
+	g_object_unref (file);
+}
+
+static void
+soup_cache_entry_delete_by_key (SoupCache *cache, const char *key)
+{
+	GFile *file;
+	char *md5, *filename;
+
+	/* Delete cache file */
+	md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, key, -1);
+	filename = g_build_filename (cache->priv->cache_dir, md5, NULL);
+	file = g_file_new_for_path (filename);
+	g_file_delete (file, NULL, NULL);
+	g_free (md5);
+	g_free (filename);
+	g_object_unref (file);
+
+	/* Remove from cache */
+	g_hash_table_remove (cache->priv->cache, key);
+
+}
+
+static void
+msg_restarted_cb (SoupMessage *msg, SoupCacheEntry *entry)
+{
+	/* FIXME: What should we do here exactly? */
+}
+
+static void
+msg_got_headers_cb (SoupMessage *msg, SoupCache *cache)
+{
+	SoupCacheability cacheable;
+
+	cacheable = get_cacheability (msg);
+
+	if (cacheable & SOUP_CACHE_CACHEABLE) {
+		/* Create a new entry, deleting any old one if
+		   present */
+		SoupCacheEntry *entry;
+
+		entry = soup_cache_entry_new (cache, msg);
+		soup_cache_entry_delete_by_key (cache, entry->key);
+
+		g_hash_table_insert (cache->priv->cache, g_strdup (entry->key), entry);
+		g_signal_connect (msg, "got-chunk", G_CALLBACK (msg_got_chunk_cb), entry);
+		g_signal_connect (msg, "restarted", G_CALLBACK (msg_restarted_cb), entry);
+	} else if (cacheable & SOUP_CACHE_INVALIDATES) {
+		char *key;
+
+		key = soup_message_get_cache_key (msg);
+		soup_cache_entry_delete_by_key (cache, key);
+		g_free (key);
+	}
+}
+
+static void
+soup_cache_entry_delete (SoupCache *cache, SoupCacheEntry *entry)
+{
+	GFile *file;
+
+	/* Delete cache file */
+	file = g_file_new_for_path (entry->filename);
+	g_file_delete (file, NULL, NULL);
+	g_object_unref (file);
+
+	/* Remove from cache */
+	g_hash_table_remove (cache->priv->cache,
+			     entry->key);
+}
+
+gboolean
+soup_cache_has_response (SoupCache *cache, SoupSession *session, SoupMessage *msg)
+{
+	char *key;
+	SoupCacheEntry *entry;
+	const char *cache_control;
+	GHashTable *hash;
+	gpointer value;
+	gboolean must_revalidate;
+	int max_age, max_stale, min_fresh;
+
+	key = soup_message_get_cache_key (msg);
+	entry = soup_cache_lookup_uri (cache, key);
+
+	/* 1. The presented Request-URI and that of stored response
+	   match */
+	if (!entry) return FALSE;
+
+	/* 2. The request method associated with the stored response
+	   allows it to be used for the presented request */
+
+	/* In practice this means we only return our resource for GET,
+	   cacheability for other methods is a TODO in the RFC
+	   (TODO: although we could return the headers for HEAD
+	   probably). */
+	if (msg->method != SOUP_METHOD_GET) return FALSE;
+
+	/* 3. Selecting request-headers nominated by the stored
+	   response (if any) match those presented. */
+
+	/* TODO */
+
+	/* 4. The presented request and stored response are free from
+	   directives that would prevent its use. */
+
+	must_revalidate = FALSE;
+	max_age = max_stale = min_fresh = -1;
+
+	cache_control = soup_message_headers_get (entry->headers, "Cache-Control");
+	if (cache_control) {
+		hash = soup_header_parse_param_list (cache_control);
+
+		if (g_hash_table_lookup (hash, "no-cache") ||
+		    g_hash_table_lookup (hash, "no-store")) {
+			g_hash_table_destroy (hash);
+			return FALSE;
+		}
+
+		value = g_hash_table_lookup (hash, "max-age");
+		if (value)
+			max_age = (int)MIN (g_ascii_strtoll (value, NULL, 10.0), G_MAXINT32);
+
+		/* max-stale can have no value set, we need to use _extended */
+		if (g_hash_table_lookup_extended (hash, "max-stale", NULL, &value)) {
+			if (value)
+				max_stale = (int)MIN (g_ascii_strtoll (value, NULL, 10.0), G_MAXINT32);
+			else
+				max_stale = G_MAXINT32;
+		}
+
+		value = g_hash_table_lookup (hash, "min-fresh");
+		if (value)
+			min_fresh = (int)MIN (g_ascii_strtoll (value, NULL, 10.0), G_MAXINT32);
+
+		/* must-revalidate has no value set, so use _extended */
+		must_revalidate = (gboolean)g_hash_table_lookup_extended (hash, "must-revalidate", NULL, NULL);
+
+		g_hash_table_destroy (hash);
+
+		if (max_age != -1) {
+			guint current_age = soup_cache_entry_get_current_age (entry);
+
+			/* If we are over max-age and max-stale is not set, do
+			   not use the value from the cache */
+			if (max_age <= current_age && max_stale == -1)
+				return FALSE;
+		}
+	}
+
+	/* 5. The stored response is either: fresh, allowed to be
+	   served stale or succesfully validated */
+	if (!soup_cache_entry_is_fresh_enough (entry, min_fresh)) {
+		/* Not fresh, can it be served stale? */
+		if (max_stale != -1) {
+			/* G_MAXINT32 means we accept any staleness */
+			if (max_stale == G_MAXINT32)
+				return TRUE;
+
+			if ((soup_cache_entry_get_current_age (entry) - entry->freshness_lifetime) <= max_stale)
+				return TRUE;
+		}
+
+		if (must_revalidate) {
+			/* If it's not fresh and it can't be served stale,
+			   remove it from the cache */
+			/* TODO: Add re-validation with the server */
+			soup_cache_entry_delete (cache, entry);
+		}
+
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+void
+soup_cache_send_response (SoupCache *cache, SoupSession *session, SoupMessage *msg)
+{
+	char *key;
+	SoupCacheEntry *entry;
+	SoupBuffer *buffer;
+	char *current_age;
+
+	key = soup_message_get_cache_key (msg);
+	entry = soup_cache_lookup_uri (cache, key);
+	g_return_if_fail (entry);
+
+	/* Headers */
+	msg->response_headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
+	soup_message_headers_foreach (entry->headers,
+				      (SoupMessageHeadersForeachFunc)headers_foreach,
+				      msg->response_headers);
+
+	/* Add 'Age' header with the current age */
+	current_age = g_strdup_printf ("%d", soup_cache_entry_get_current_age (entry));
+	soup_message_headers_replace (msg->response_headers,
+				      "Age",
+				      current_age);
+	g_free (current_age);
+
+	g_signal_emit_by_name (msg, "got-headers", NULL);
+
+	/* Data */
+	msg->response_body = soup_message_body_new ();
+
+	/* Do not try to read anything if the length of the
+	   resource is 0 */
+	if (entry->length) {
+		char *data;
+		gsize length;
+
+		g_file_get_contents (entry->filename, &data, &length, NULL);
+
+		if (data && length) {
+			buffer = soup_buffer_new (SOUP_MEMORY_TEMPORARY, data, length);
+			soup_message_body_append_buffer (msg->response_body, buffer);
+			g_signal_emit_by_name (msg, "got-chunk", buffer, NULL);
+			soup_buffer_free (buffer);
+		}
+	}
+
+	soup_message_got_body (msg);
+	soup_message_finished (msg);
+}
+
+static void
+request_queued (SoupSessionFeature *feature, SoupSession *session,
+		SoupMessage *msg)
+{
+	g_signal_connect (msg, "got-headers", G_CALLBACK (msg_got_headers_cb), feature);
+}
+
+static void
+soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface,
+                                 gpointer interface_data)
+{
+	feature_interface->request_queued = request_queued;
+}
+
+static void
+soup_cache_init (SoupCache *cache)
+{
+	SoupCachePrivate *priv;
+
+	priv = cache->priv = SOUP_CACHE_GET_PRIVATE (cache);
+
+	priv->cache = g_hash_table_new_full (g_str_hash,
+					     g_str_equal,
+					     (GDestroyNotify)g_free,
+					     (GDestroyNotify)soup_cache_entry_free);
+}
+
+static void
+soup_cache_finalize (GObject *object)
+{
+	SoupCachePrivate *priv;
+
+	priv = SOUP_CACHE (object)->priv;
+
+	g_hash_table_destroy (priv->cache);
+	g_free (priv->cache_dir);
+
+	G_OBJECT_CLASS (soup_cache_parent_class)->finalize (object);
+}
+
+static void
+soup_cache_set_property (GObject *object, guint prop_id,
+			 const GValue *value, GParamSpec *pspec)
+{
+	SoupCachePrivate *priv = SOUP_CACHE (object)->priv;
+
+	switch (prop_id) {
+	case PROP_CACHE_DIR:
+		priv->cache_dir = g_value_dup_string (value);
+		/* Create directory if it does not exist (FIXME: should we?) */
+		if (!g_file_test (priv->cache_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))
+			g_mkdir_with_parents (priv->cache_dir, 0700);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+soup_cache_get_property (GObject *object, guint prop_id,
+			 GValue *value, GParamSpec *pspec)
+{
+	SoupCachePrivate *priv = SOUP_CACHE (object)->priv;
+
+	switch (prop_id) {
+	case PROP_CACHE_DIR:
+		g_value_set_string (value, priv->cache_dir);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static GObject *
+soup_cache_constructor (GType                  type,
+			guint                  n_construct_properties,
+			GObjectConstructParam *construct_properties)
+{
+	GObject *cache;
+	SoupCachePrivate *priv;
+
+	cache = G_OBJECT_CLASS (soup_cache_parent_class)->constructor
+		(type, n_construct_properties, construct_properties);
+
+	priv = SOUP_CACHE (cache)->priv;
+
+	if (!priv->cache_dir) {
+		/* Set a default cache dir, different for each user */
+		priv->cache_dir = g_build_filename (g_get_user_cache_dir (),
+						    "httpcache",
+						    NULL);
+		if (!g_file_test (priv->cache_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))
+			g_mkdir_with_parents (priv->cache_dir, 0700);
+	}
+	
+	return cache;
+}
+
+static void
+soup_cache_class_init (SoupCacheClass *cache_class)
+{
+	GObjectClass *gobject_class = (GObjectClass*)cache_class;
+
+	gobject_class->finalize = soup_cache_finalize;
+	gobject_class->constructor = soup_cache_constructor;
+	gobject_class->set_property = soup_cache_set_property;
+	gobject_class->get_property = soup_cache_get_property;
+
+	g_object_class_install_property(gobject_class, PROP_CACHE_DIR,
+					g_param_spec_string("cache-dir",
+							    "Cache directory",
+							    "The directory to store the cache files",
+							    NULL,
+							    G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_type_class_add_private(cache_class, sizeof (SoupCachePrivate));
+}
+
+/**
+ * soup_cache_new:
+ * @cache_dir: the directory to store the cached data, or %NULL to use the default one
+ *
+ * Creates a new #SoupCache.
+ *
+ * Returns: a new #SoupCache
+ *
+ * Since: 2.28
+ **/
+SoupCache *
+soup_cache_new (const char *cache_dir)
+{
+	return g_object_new (SOUP_TYPE_CACHE,
+			     "cache-dir", cache_dir,
+			     NULL);
+}
+
diff --git a/libsoup/soup-cache.h b/libsoup/soup-cache.h
new file mode 100644
index 0000000..1efb5bb
--- /dev/null
+++ b/libsoup/soup-cache.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2009 Igalia S.L.
+ */
+
+#ifndef SOUP_CACHE_H
+#define SOUP_CACHE_H 1
+
+#include <libsoup/soup-types.h>
+
+G_BEGIN_DECLS
+
+#define SOUP_TYPE_CACHE		   (soup_cache_get_type ())
+#define SOUP_CACHE(obj)		   (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_CACHE, SoupCache))
+#define SOUP_CACHE_CLASS(klass)	   (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_CACHE, SoupCacheClass))
+#define SOUP_IS_CACHE(obj)	   (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_CACHE))
+#define SOUP_IS_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_CACHE))
+#define SOUP_CACHE_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_CACHE, SoupCacheClass))
+
+typedef enum {
+	SOUP_CACHE_CACHEABLE   = (1 << 0),
+	SOUP_CACHE_UNCACHEABLE = (1 << 1),
+	SOUP_CACHE_INVALIDATES = (1 << 2)
+} SoupCacheability;
+
+typedef struct _SoupCachePrivate SoupCachePrivate;
+
+typedef struct {
+	GObject parent_instance;
+
+	SoupCachePrivate *priv;
+} SoupCache;
+
+typedef struct {
+	GObjectClass parent_class;
+
+	/* Padding for future expansion */
+	void (*_libsoup_reserved1) (void);
+	void (*_libsoup_reserved2) (void);
+	void (*_libsoup_reserved3) (void);
+} SoupCacheClass;
+
+GType	       soup_cache_get_type	(void);
+SoupCache     *soup_cache_new		(const char *cache_dir);
+gboolean       soup_cache_has_response	(SoupCache *cache, SoupSession *session, SoupMessage *msg);
+void	       soup_cache_send_response (SoupCache *cache, SoupSession *session, SoupMessage *msg);
+
+G_END_DECLS
+
+
+#endif /* SOUP_CACHE_H */
+
diff --git a/libsoup/soup-session-async.c b/libsoup/soup-session-async.c
index d9d17e3..811704a 100644
--- a/libsoup/soup-session-async.c
+++ b/libsoup/soup-session-async.c
@@ -360,11 +360,45 @@ do_idle_run_queue (SoupSession *session)
 	}
 }
 
+typedef struct _QueueMessageData {
+	SoupSession *session;
+	SoupMessage *msg;
+	SoupSessionCallback callback;
+	gpointer user_data;
+	SoupCache *cache;
+} QueueMessageData;
+
+static gboolean
+had_cache (QueueMessageData *data)
+{
+	soup_cache_send_response (data->cache, data->session, data->msg);
+	data->callback (data->session, data->msg, data->user_data);
+	g_slice_free (QueueMessageData, data);
+	return FALSE;
+}
+
 static void
 queue_message (SoupSession *session, SoupMessage *req,
 	       SoupSessionCallback callback, gpointer user_data)
 {
 	SoupMessageQueueItem *item;
+	SoupCache *cache;
+
+	cache = soup_session_get_cache (session);
+
+	if (cache && soup_cache_has_response (cache, session, req)) {
+		QueueMessageData* data;
+
+		data = g_slice_new (QueueMessageData);
+		data->session = session;
+		data->msg = req;
+		data->callback = callback;
+		data->user_data = user_data;
+		data->cache = cache;
+
+		g_idle_add ((GSourceFunc)had_cache, data);
+		return;
+	}
 
 	SOUP_SESSION_CLASS (soup_session_async_parent_class)->queue_message (session, req, callback, user_data);
 
diff --git a/libsoup/soup-session-private.h b/libsoup/soup-session-private.h
index f8b13d5..f0c478c 100644
--- a/libsoup/soup-session-private.h
+++ b/libsoup/soup-session-private.h
@@ -7,6 +7,7 @@
 #define SOUP_SESSION_PRIVATE_H 1
 
 #include "soup-session.h"
+#include "soup-cache.h"
 #include "soup-connection.h"
 #include "soup-message-queue.h"
 #include "soup-proxy-resolver.h"
@@ -24,6 +25,7 @@ SoupConnection   *soup_session_get_connection       (SoupSession *session,
 gboolean          soup_session_try_prune_connection (SoupSession *session);
 
 SoupProxyResolver *soup_session_get_proxy_resolver  (SoupSession *session);
+SoupCache         *soup_session_get_cache           (SoupSession *session);
 
 G_END_DECLS
 
diff --git a/libsoup/soup-session.c b/libsoup/soup-session.c
index fa66fd7..570e192 100644
--- a/libsoup/soup-session.c
+++ b/libsoup/soup-session.c
@@ -18,6 +18,7 @@
 #include "soup-auth-basic.h"
 #include "soup-auth-digest.h"
 #include "soup-auth-manager-ntlm.h"
+#include "soup-cache.h"
 #include "soup-connection.h"
 #include "soup-marshal.h"
 #include "soup-message-private.h"
@@ -65,6 +66,7 @@ typedef struct {
 
 typedef struct {
 	SoupProxyResolver *proxy_resolver;
+	SoupCache *cache;
 
 	char *ssl_ca_file;
 	SoupSSLCredentials *ssl_creds;
@@ -1490,6 +1492,9 @@ soup_session_add_feature (SoupSession *session, SoupSessionFeature *feature)
 
 	if (SOUP_IS_PROXY_RESOLVER (feature))
 		priv->proxy_resolver = SOUP_PROXY_RESOLVER (feature);
+
+	if (SOUP_IS_CACHE (feature))
+		priv->cache = SOUP_CACHE (feature);
 }
 
 /**
@@ -1542,6 +1547,9 @@ soup_session_remove_feature (SoupSession *session, SoupSessionFeature *feature)
 
 		if (feature == (SoupSessionFeature *)priv->proxy_resolver)
 			priv->proxy_resolver = NULL;
+
+		if (feature == (SoupSessionFeature *)priv->cache)
+			priv->cache = NULL;
 	}
 }
 
@@ -1642,3 +1650,11 @@ soup_session_get_proxy_resolver (SoupSession *session)
 
 	return priv->proxy_resolver;
 }
+
+SoupCache *
+soup_session_get_cache (SoupSession *session)
+{
+	SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session);
+
+	return priv->cache;
+}
diff --git a/libsoup/soup.h b/libsoup/soup.h
index 496a4c1..4ac0991 100644
--- a/libsoup/soup.h
+++ b/libsoup/soup.h
@@ -15,6 +15,7 @@ extern "C" {
 #include <libsoup/soup-auth-domain.h>
 #include <libsoup/soup-auth-domain-basic.h>
 #include <libsoup/soup-auth-domain-digest.h>
+#include <libsoup/soup-cache.h>
 #include <libsoup/soup-cookie.h>
 #include <libsoup/soup-cookie-jar.h>
 #include <libsoup/soup-cookie-jar-text.h>
