diff --git a/libsoup/Makefile.am b/libsoup/Makefile.am
index 949f243..2d3a6ea 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-content-sniffer.h  \
 	soup-cookie.h		\
 	soup-cookie-jar.h	\
 	soup-cookie-jar-text.h	\
@@ -119,6 +120,7 @@ libsoup_2_4_la_SOURCES =		\
 	soup-auth-manager-ntlm.c	\
 	soup-connection.h		\
 	soup-connection.c		\
+	soup-content-sniffer.c		\
 	soup-cookie.c			\
 	soup-cookie-jar.c		\
 	soup-cookie-jar-text.c		\
diff --git a/libsoup/soup-content-sniffer.c b/libsoup/soup-content-sniffer.c
new file mode 100644
index 0000000..492066a
--- /dev/null
+++ b/libsoup/soup-content-sniffer.c
@@ -0,0 +1,218 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-content-sniffer.c
+ *
+ * Copyright (C) 2009 Gustavo Noronha Silva.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <string.h>
+#include <gio/gio.h>
+
+#include "soup-content-sniffer.h"
+#include "soup-enum-types.h"
+#include "soup-message.h"
+#include "soup-message-private.h"
+#include "soup-session-feature.h"
+#include "soup-uri.h"
+
+/**
+ * SECTION:soup-content-sniffer
+ * @short_description: Content sniffing for #SoupSession
+ *
+ * A #SoupContentSniffer tries to detect the actual content type of
+ * the files that are being downloaded by looking at some of the data
+ * before the #SoupMessage emits its #SoupMessage::got-headers signal.
+ * #SoupContentSniffer implements #SoupSessionFeature, so you can add
+ * content sniffing to a session with soup_session_add_feature() or
+ * soup_session_add_feature_by_type().
+ *
+ * Since: 2.27.x
+ **/
+
+static void soup_content_sniffer_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
+static void request_queued (SoupSessionFeature *feature, SoupSession *session, SoupMessage *msg);
+
+G_DEFINE_TYPE_WITH_CODE (SoupContentSniffer, soup_content_sniffer, G_TYPE_OBJECT,
+			 G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
+						soup_content_sniffer_session_feature_init))
+
+enum {
+	PROP_0,
+
+	PROP_OPERATION_MODE,
+
+	LAST_PROP
+};
+
+struct _SoupContentSnifferPrivate {
+	SoupContentSnifferOperationMode operation_mode;
+};
+
+#define SOUP_CONTENT_SNIFFER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_CONTENT_SNIFFER, SoupContentSnifferPrivate))
+
+static void set_property (GObject *object, guint prop_id,
+			  const GValue *value, GParamSpec *pspec);
+static void get_property (GObject *object, guint prop_id,
+			  GValue *value, GParamSpec *pspec);
+
+static void
+soup_content_sniffer_init (SoupContentSniffer *content_sniffer)
+{
+	SoupContentSnifferPrivate *priv = SOUP_CONTENT_SNIFFER_GET_PRIVATE (content_sniffer);
+	content_sniffer->priv = priv;
+}
+
+static void
+soup_content_sniffer_class_init (SoupContentSnifferClass *content_sniffer_class)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (content_sniffer_class);
+
+	object_class->set_property = set_property;
+	object_class->get_property = get_property;
+
+	/**
+	 * SoupContentSniffer:sniff-type:
+	 *
+	 * Type of content sniffing that should be done by libsoup.
+	 *
+	 * Since: 2.27.x
+	 */
+	g_object_class_install_property (
+		object_class, PROP_OPERATION_MODE,
+		g_param_spec_enum ("operation-mode",
+				   "Operation Mode",
+				   "Defines how sniffing should be done",
+				   SOUP_TYPE_CONTENT_SNIFFER_OPERATION_MODE,
+				   SOUP_CONTENT_SNIFFER_OPERATION_MODE_HTML5,
+				   G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_type_class_add_private (content_sniffer_class, sizeof (SoupContentSnifferPrivate));
+
+}
+
+static void
+soup_content_sniffer_session_feature_init (SoupSessionFeatureInterface *feature_interface,
+					   gpointer interface_data)
+{
+	feature_interface->request_queued = request_queued;
+}
+
+static void
+set_property (GObject *object, guint prop_id,
+	      const GValue *value, GParamSpec *pspec)
+{
+	SoupContentSnifferPrivate *priv = SOUP_CONTENT_SNIFFER(object)->priv;
+
+	switch (prop_id) {
+	case PROP_OPERATION_MODE:
+		priv->operation_mode = g_value_get_enum (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+get_property (GObject *object, guint prop_id,
+	      GValue *value, GParamSpec *pspec)
+{
+	SoupContentSnifferPrivate *priv = SOUP_CONTENT_SNIFFER(object)->priv;
+
+	switch (prop_id) {
+	case PROP_OPERATION_MODE:
+		g_value_set_enum (value, priv->operation_mode);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+/**
+ * soup_content_sniffer_new:
+ *
+ * Creates a new #SoupContentSniffer.
+ *
+ * Returns: a new #SoupContentSniffer
+ *
+ * Since: 2.27.x
+ **/
+SoupContentSniffer *
+soup_content_sniffer_new (SoupContentSnifferOperationMode operation_mode)
+{
+	return g_object_new (SOUP_TYPE_CONTENT_SNIFFER, "operation-mode", operation_mode, NULL);
+}
+
+static void
+html5_sniff (SoupMessage *msg, const char* data, gsize length)
+{
+	SoupURI *uri;
+	char *uri_path;
+	gboolean uncertain;
+	char *content_type;
+	char *mime_type;
+
+	uri = soup_message_get_uri (msg);
+	uri_path = soup_uri_to_string (uri, TRUE);
+
+	content_type= g_content_type_guess (uri_path, (const guchar*)data, length, &uncertain);
+	mime_type = g_content_type_get_mime_type (content_type);
+
+	g_free (uri_path);
+
+	if (!uncertain)
+		soup_message_content_sniffed (msg, mime_type);
+
+	g_free (content_type);
+	g_free (mime_type);
+
+}
+
+void
+soup_content_sniffer_sniff (SoupContentSniffer *sniffer,
+			    SoupMessage *msg,
+			    const char* data,
+			    gsize length)
+{
+	SoupContentSnifferPrivate *priv = sniffer->priv;
+
+	switch (priv->operation_mode) {
+	case SOUP_CONTENT_SNIFFER_OPERATION_MODE_HTML5:
+		html5_sniff (msg, data, length);
+		break;
+	default:
+		g_critical ("Invalid content sniffing operation mode.");
+		soup_message_content_sniffed (msg, NULL);
+		break;
+	}
+}
+
+static void
+soup_content_sniffer_got_headers_cb(SoupMessage *msg, SoupContentSniffer *sniffer)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+	const char *content_type = soup_message_headers_get_content_type (msg->response_headers, NULL);
+
+	g_debug ("content-type: %s", content_type);
+	if ((content_type == NULL)
+	    || (strcmp (content_type, "application/octet-stream") == 0)
+	    || (strcmp (content_type, "text/plain") == 0)) {
+		priv->should_sniff_content = TRUE;
+	}
+}
+
+static void
+request_queued (SoupSessionFeature *feature, SoupSession *session,
+		SoupMessage *msg)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+	priv->sniffer = SOUP_CONTENT_SNIFFER(g_object_ref(G_OBJECT(feature)));
+	g_signal_connect (msg, "got-headers",
+			  G_CALLBACK(soup_content_sniffer_got_headers_cb),
+			  feature);
+}
diff --git a/libsoup/soup-content-sniffer.h b/libsoup/soup-content-sniffer.h
new file mode 100644
index 0000000..0e76e83
--- /dev/null
+++ b/libsoup/soup-content-sniffer.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2009 Gustavo Noronha Silva.
+ */
+
+#ifndef SOUP_CONTENT_SNIFFER_H
+#define SOUP_CONTENT_SNIFFER_H 1
+
+#include <libsoup/soup-types.h>
+
+G_BEGIN_DECLS
+
+#define SOUP_TYPE_CONTENT_SNIFFER            (soup_content_sniffer_get_type ())
+#define SOUP_CONTENT_SNIFFER(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_CONTENT_SNIFFER, SoupContentSniffer))
+#define SOUP_CONTENT_SNIFFER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_CONTENT_SNIFFER, SoupContentSnifferClass))
+#define SOUP_IS_CONTENT_SNIFFER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_CONTENT_SNIFFER))
+#define SOUP_IS_CONTENT_SNIFFER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_CONTENT_SNIFFER))
+#define SOUP_CONTENT_SNIFFER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_CONTENT_SNIFFER, SoupContentSnifferClass))
+
+typedef struct _SoupContentSnifferPrivate SoupContentSnifferPrivate;
+
+typedef struct {
+	GObject parent;
+
+	SoupContentSnifferPrivate *priv;
+} SoupContentSniffer;
+
+typedef struct {
+	GObjectClass parent_class;
+
+	/* Padding for future expansion */
+	void (*_libsoup_reserved1) (void);
+	void (*_libsoup_reserved2) (void);
+	void (*_libsoup_reserved3) (void);
+	void (*_libsoup_reserved4) (void);
+	void (*_libsoup_reserved5) (void);
+} SoupContentSnifferClass;
+
+typedef enum {
+	SOUP_CONTENT_SNIFFER_OPERATION_MODE_HTML5
+} SoupContentSnifferOperationMode;
+
+GType          soup_content_sniffer_get_type      (void);
+
+SoupContentSniffer *soup_content_sniffer_new      (SoupContentSnifferOperationMode operation_mode);
+
+void           soup_content_sniffer_sniff         (SoupContentSniffer *sniffer,
+						   SoupMessage *msg,
+						   const char *data,
+						   const gsize length);
+
+G_END_DECLS
+
+#endif /* SOUP_CONTENT_SNIFFER_H */
diff --git a/libsoup/soup-marshal.list b/libsoup/soup-marshal.list
index 1a43570..785fd6f 100644
--- a/libsoup/soup-marshal.list
+++ b/libsoup/soup-marshal.list
@@ -6,3 +6,4 @@ NONE:OBJECT,OBJECT
 NONE:OBJECT,POINTER
 NONE:BOXED,BOXED
 NONE:OBJECT,OBJECT,BOOLEAN
+NONE:STRING
diff --git a/libsoup/soup-message-io.c b/libsoup/soup-message-io.c
index 8e04b66..e568eac 100644
--- a/libsoup/soup-message-io.c
+++ b/libsoup/soup-message-io.c
@@ -18,6 +18,7 @@
 #include "soup-misc.h"
 #include "soup-socket.h"
 #include "soup-ssl.h"
+#include "soup-uri.h"
 
 typedef enum {
 	SOUP_MESSAGE_IO_CLIENT,
@@ -52,6 +53,8 @@ typedef struct {
 	GByteArray           *read_meta_buf;
 	SoupMessageBody      *read_body;
 	goffset               read_length;
+	gboolean              delay_got_chunks;
+	GString              *delayed_chunk_data;
 
 	SoupMessageIOState    write_state;
 	SoupEncoding          write_encoding;
@@ -78,6 +81,7 @@ typedef struct {
 #define SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK { gboolean cancelled; g_object_ref (msg);
 #define SOUP_MESSAGE_IO_RETURN_IF_CANCELLED_OR_PAUSED cancelled = (priv->io_data != io); g_object_unref (msg); if (cancelled || (!io->read_tag && !io->write_tag)) return; }
 #define SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED(val) cancelled = (priv->io_data != io); g_object_unref (msg); if (cancelled || (!io->read_tag && !io->write_tag)) return val; }
+#define SOUP_MESSAGE_IO_IS_CANCELLED (priv->io_data != io)
 
 #define RESPONSE_BLOCK_SIZE 8192
 
@@ -172,6 +176,7 @@ soup_message_io_finished (SoupMessage *msg)
 }
 
 static void io_read (SoupSocket *sock, SoupMessage *msg);
+static void io_content_sniff(SoupMessage *msg);
 
 static void
 io_error (SoupSocket *sock, SoupMessage *msg, GError *error)
@@ -324,10 +329,40 @@ read_body_chunk (SoupMessage *msg)
 
 			io->read_length -= nread;
 
-			SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
-			soup_message_got_chunk (msg, buffer);
-			soup_buffer_free (buffer);
-			SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED (FALSE);
+			g_debug("io->delay_got_chunks: %d", io->delay_got_chunks);
+			if (io->delay_got_chunks) {
+				if (io->delayed_chunk_data)
+					io->delayed_chunk_data = g_string_append_len(io->delayed_chunk_data,
+										     buffer->data,
+										     buffer->length);
+				else
+					io->delayed_chunk_data = g_string_new_len(buffer->data,
+										  buffer->length);
+
+				/* FIXME: we probably want the feature to define this number; the HTML5
+				 * spec says the client may wait for 512 or more bytes.
+				 */
+				if (io->delayed_chunk_data->len > 512) {
+					SoupBuffer *sniffed_buffer = soup_buffer_new (SOUP_MEMORY_TEMPORARY,
+										      io->delayed_chunk_data->str,
+										      io->delayed_chunk_data->len);
+
+					io_content_sniff(msg);
+					io->delay_got_chunks = FALSE;
+
+					SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+					soup_message_got_chunk (msg, sniffed_buffer);
+					soup_buffer_free (sniffed_buffer);
+					g_string_free(io->delayed_chunk_data, TRUE);
+					io->delayed_chunk_data = NULL;
+					SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED (FALSE);
+				}
+			} else {
+				SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+				soup_message_got_chunk (msg, buffer);
+				soup_buffer_free (buffer);
+				SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED (FALSE);
+			}
 			continue;
 		}
 
@@ -353,6 +388,15 @@ read_body_chunk (SoupMessage *msg)
 	return TRUE;
 }
 
+static void
+io_content_sniff(SoupMessage *msg)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+	SoupMessageIOData *io = priv->io_data;
+
+	soup_content_sniffer_sniff(priv->sniffer, msg, (const gchar*)io->delayed_chunk_data->str, io->delayed_chunk_data->len);
+}
+
 /* Attempts to write @len bytes from @data. See the note at
  * read_metadata() for an explanation of the return value.
  */
@@ -765,6 +809,10 @@ io_read (SoupSocket *sock, SoupMessage *msg)
 		} else {
 			SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
 			soup_message_got_headers (msg);
+			if (priv->should_sniff_content)
+				io->delay_got_chunks = TRUE;
+			else
+				soup_message_content_sniffed(msg, NULL);
 			SOUP_MESSAGE_IO_RETURN_IF_CANCELLED_OR_PAUSED;
 		}
 		break;
@@ -782,6 +830,29 @@ io_read (SoupSocket *sock, SoupMessage *msg)
 			return;
 
 	got_body:
+		/* A chunk of data may have been read and the emission
+		 * of got_chunk delayed because we wanted to wait for
+		 * more chunks to arrive, for doing content sniffing,
+		 * but the body was too small, so we need to check if
+		 * an emission is in order here, along with the
+		 * sniffing, of course.
+		 */
+		if (io->delayed_chunk_data) {
+			SoupBuffer *sniffed_buffer = soup_buffer_new (SOUP_MEMORY_TEMPORARY,
+								      io->delayed_chunk_data->str,
+								      io->delayed_chunk_data->len);
+
+			io_content_sniff(msg);
+			io->delay_got_chunks = FALSE;
+
+			SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+			soup_message_got_chunk (msg, sniffed_buffer);
+			soup_buffer_free (sniffed_buffer);
+			g_string_free(io->delayed_chunk_data, TRUE);
+			io->delayed_chunk_data = NULL;
+			SOUP_MESSAGE_IO_RETURN_IF_CANCELLED_OR_PAUSED;
+		}
+
 		io->read_state = SOUP_MESSAGE_IO_STATE_FINISHING;
 
 		SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
@@ -885,6 +956,9 @@ new_iostate (SoupMessage *msg, SoupSocket *sock, SoupMessageIOMode mode,
 	io->read_state  = SOUP_MESSAGE_IO_STATE_NOT_STARTED;
 	io->write_state = SOUP_MESSAGE_IO_STATE_NOT_STARTED;
 
+	if (priv->should_sniff_content)
+		io->delay_got_chunks = TRUE;
+
 	if (priv->io_data)
 		soup_message_io_cleanup (msg);
 	priv->io_data = io;
diff --git a/libsoup/soup-message-private.h b/libsoup/soup-message-private.h
index f47251a..a3e8c44 100644
--- a/libsoup/soup-message-private.h
+++ b/libsoup/soup-message-private.h
@@ -9,6 +9,7 @@
 #include "soup-message.h"
 #include "soup-auth.h"
 #include "soup-connection.h"
+#include "soup-content-sniffer.h"
 
 typedef enum {
 	SOUP_MESSAGE_IO_STATUS_IDLE,
@@ -28,6 +29,8 @@ typedef struct {
 
 	guint              msg_flags;
 	gboolean           server_side;
+	gboolean           should_sniff_content;
+	SoupContentSniffer *sniffer;
 
 	SoupHTTPVersion    http_version, orig_http_version;
 
diff --git a/libsoup/soup-message.c b/libsoup/soup-message.c
index 5475bb7..b99823d 100644
--- a/libsoup/soup-message.c
+++ b/libsoup/soup-message.c
@@ -99,6 +99,7 @@ enum {
 	GOT_HEADERS,
 	GOT_CHUNK,
 	GOT_BODY,
+	CONTENT_SNIFFED,
 
 	RESTARTED,
 	FINISHED,
@@ -165,6 +166,9 @@ finalize (GObject *object)
 	if (priv->proxy_auth)
 		g_object_unref (priv->proxy_auth);
 
+	if (priv->sniffer)
+		g_object_unref (priv->sniffer);
+
 	soup_message_body_free (msg->request_body);
 	soup_message_headers_free (msg->request_headers);
 	soup_message_body_free (msg->response_body);
@@ -402,6 +406,42 @@ soup_message_class_init (SoupMessageClass *message_class)
 			      G_TYPE_NONE, 0);
 
 	/**
+	 * SoupMessage::content-sniffed:
+	 * @msg: the message
+	 * @type: the content type that we got from sniffing
+	 *
+	 * This signal is emitted after %got-headers, and before the
+	 * first %got-chunk. If content sniffing is disabled, or no
+	 * content sniffing will be performed, due to the sniffer
+	 * deciding to trust the Content-Type sent by the server, this
+	 * signal is emitted immediately after %got_headers, and @type
+	 * is %NULL.
+	 *
+	 * If the #SoupContentSniffer feature is enabled, and the
+	 * sniffer decided to perform sniffing, the first %got_chunk
+	 * emission may be delayed, so that the sniffer has enough
+	 * data to correctly sniff the content. It notified the
+	 * library user that the content has been sniffed, and allows
+	 * it to change the header contents in the message, if
+	 * desired.
+	 *
+	 * After this signal is emitted, the data that was spooled so
+	 * that sniffing could be done is delivered on the first
+	 * emission of %got_chunk.
+	 *
+	 * Since: 2.27.x
+	 **/
+	signals[CONTENT_SNIFFED] =
+		g_signal_new ("content_sniffed",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_FIRST,
+			      0,
+			      NULL, NULL,
+			      soup_marshal_NONE__STRING,
+			      G_TYPE_NONE, 1,
+			      G_TYPE_STRING);
+
+	/**
 	 * SoupMessage::restarted:
 	 * @msg: the message
 	 *
@@ -858,6 +898,24 @@ soup_message_got_body (SoupMessage *msg)
 	g_signal_emit (msg, signals[GOT_BODY], 0);
 }
 
+/**
+ * soup_message_content_sniffed:
+ * @msg: a #SoupMessage
+ * @type: a string with the sniffed content type
+ *
+ * Emits the %content_sniffed signal, indicating that the IO layer
+ * finished sniffing the content type for @msg. If content sniffing is
+ * disabled, or no content sniffing will be performed, due to the
+ * sniffer deciding to trust the Content-Type sent by the server, this
+ * signal is emitted immediately after %got_headers.
+ **/
+void
+soup_message_content_sniffed (SoupMessage *msg, const char *content_type)
+{
+	g_debug("content_sniffed: %s", content_type);
+	g_signal_emit (msg, signals[CONTENT_SNIFFED], 0, content_type);
+}
+
 static void
 restarted (SoupMessage *req)
 {
diff --git a/libsoup/soup-message.h b/libsoup/soup-message.h
index 1b850be..4a51466 100644
--- a/libsoup/soup-message.h
+++ b/libsoup/soup-message.h
@@ -155,6 +155,7 @@ void soup_message_got_informational   (SoupMessage *msg);
 void soup_message_got_headers         (SoupMessage *msg);
 void soup_message_got_chunk           (SoupMessage *msg, SoupBuffer *chunk);
 void soup_message_got_body            (SoupMessage *msg);
+void soup_message_content_sniffed     (SoupMessage *msg, const char *content_type);
 void soup_message_restarted           (SoupMessage *msg);
 void soup_message_finished            (SoupMessage *msg);
 
diff --git a/libsoup/soup.h b/libsoup/soup.h
index 496a4c1..ddb73f7 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-content-sniffer.h>
 #include <libsoup/soup-cookie.h>
 #include <libsoup/soup-cookie-jar.h>
 #include <libsoup/soup-cookie-jar-text.h>
