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..4a23b35
--- /dev/null
+++ b/libsoup/soup-content-sniffer.c
@@ -0,0 +1,158 @@
+/* -*- 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 char* sniff (SoupContentSniffer *sniffer, SoupMessage *msg, SoupBuffer *buffer, gboolean *uncertain);
+static gsize get_buffer_size (SoupContentSniffer *sniffer);
+
+static void soup_content_sniffer_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
+
+static void request_queued (SoupSessionFeature *feature, SoupSession *session, SoupMessage *msg);
+static void request_unqueued (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))
+
+struct _SoupContentSnifferPrivate {
+	/* placeholder */
+	void *ptr;
+};
+
+#define SOUP_CONTENT_SNIFFER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_CONTENT_SNIFFER, SoupContentSnifferPrivate))
+
+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)
+{
+	content_sniffer_class->sniff = sniff;
+	content_sniffer_class->get_buffer_size = get_buffer_size;
+
+	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;
+	feature_interface->request_unqueued = request_unqueued;
+}
+
+/**
+ * soup_content_sniffer_new:
+ *
+ * Creates a new #SoupContentSniffer.
+ *
+ * Returns: a new #SoupContentSniffer
+ *
+ * Since: 2.27.x
+ **/
+SoupContentSniffer *
+soup_content_sniffer_new ()
+{
+	return g_object_new (SOUP_TYPE_CONTENT_SNIFFER, NULL);
+}
+
+static char*
+sniff (SoupContentSniffer *sniffer, SoupMessage *msg, SoupBuffer *buffer, gboolean *uncertain)
+{
+	SoupURI *uri;
+	char *uri_path;
+	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*)buffer->data, buffer->length, uncertain);
+	mime_type = g_content_type_get_mime_type (content_type);
+
+	g_free (uri_path);
+	g_free (content_type);
+
+	return mime_type;
+}
+
+static gsize
+get_buffer_size (SoupContentSniffer *sniffer)
+{
+	return 512;
+}
+
+static void
+soup_content_sniffer_got_headers_cb (SoupMessage *msg, SoupContentSniffer *sniffer)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+	SoupContentSnifferClass *content_sniffer_class = SOUP_CONTENT_SNIFFER_GET_CLASS (sniffer);
+	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;
+		priv->bytes_for_sniffing = content_sniffer_class->get_buffer_size (sniffer);
+	}
+}
+
+static void
+request_queued (SoupSessionFeature *feature, SoupSession *session,
+		SoupMessage *msg)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+
+	priv->sniffer = g_object_ref (feature);
+	g_signal_connect (msg, "got-headers",
+			  G_CALLBACK (soup_content_sniffer_got_headers_cb),
+			  feature);
+}
+
+static void
+request_unqueued (SoupSessionFeature *feature, SoupSession *session,
+		  SoupMessage *msg)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+
+	g_object_unref (priv->sniffer);
+	priv->sniffer = NULL;
+
+	g_signal_handlers_disconnect_by_func (msg, 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..77123ed
--- /dev/null
+++ b/libsoup/soup-content-sniffer.h
@@ -0,0 +1,56 @@
+/* -*- 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>
+#include <libsoup/soup-message-body.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;
+
+	char* (*sniff)              (SoupContentSniffer *sniffer,
+				     SoupMessage *msg,
+				     SoupBuffer *buffer,
+				     gboolean *uncertain);
+	gsize (*get_buffer_size)    (SoupContentSniffer *sniffer);
+
+	/* 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;
+
+GType               soup_content_sniffer_get_type (void);
+
+SoupContentSniffer *soup_content_sniffer_new      (void);
+
+void                soup_content_sniffer_sniff    (SoupContentSniffer *sniffer,
+						   SoupMessage *msg,
+						   SoupBuffer *buffer);
+
+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..2be2706 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,
@@ -53,6 +54,11 @@ typedef struct {
 	SoupMessageBody      *read_body;
 	goffset               read_length;
 
+	gboolean              acked_content_sniff_decision;
+	gboolean              delay_got_chunks;
+	SoupMessageBody      *delayed_chunk_data;
+	gsize                 delayed_chunk_length;
+
 	SoupMessageIOState    write_state;
 	SoupEncoding          write_encoding;
 	GString              *write_buf;
@@ -105,6 +111,9 @@ soup_message_io_cleanup (SoupMessage *msg)
 	if (io->write_chunk)
 		soup_buffer_free (io->write_chunk);
 
+	if (io->delayed_chunk_data)
+		soup_message_body_free (io->delayed_chunk_data);
+
 	g_slice_free (SoupMessageIOData, io);
 }
 
@@ -207,6 +216,39 @@ io_disconnected (SoupSocket *sock, SoupMessage *msg)
 	io_error (sock, msg, NULL);
 }
 
+static gboolean
+io_sniff_content (SoupMessage *msg)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+	SoupMessageIOData *io = priv->io_data;
+	SoupBuffer *sniffed_buffer = soup_message_body_flatten (io->delayed_chunk_data);
+	SoupContentSnifferClass *content_sniffer_class = SOUP_CONTENT_SNIFFER_GET_CLASS (priv->sniffer);
+	char *sniffed_mime_type;
+	gboolean uncertain;
+
+	io->delay_got_chunks = FALSE;
+
+	sniffed_mime_type = content_sniffer_class->sniff (priv->sniffer, msg, sniffed_buffer, &uncertain);
+	if (!uncertain) {
+		SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+		soup_message_content_sniffed (msg, sniffed_mime_type);
+		g_free (sniffed_mime_type);
+		sniffed_mime_type = NULL;
+		SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED (FALSE);
+	}
+	g_free (sniffed_mime_type);
+
+	SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+	soup_message_got_chunk (msg, sniffed_buffer);
+	g_debug("delayed_chunk: %d", sniffed_buffer->length);
+	soup_buffer_free (sniffed_buffer);
+	soup_message_body_free (io->delayed_chunk_data);
+	io->delayed_chunk_data = NULL;
+	SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED (FALSE);
+
+	return TRUE;
+}
+
 /* Reads data from io->sock into io->read_meta_buf. If @to_blank is
  * %TRUE, it reads up until a blank line ("CRLF CRLF" or "LF LF").
  * Otherwise, it reads up until a single CRLF or LF.
@@ -294,6 +336,21 @@ read_body_chunk (SoupMessage *msg)
 	GError *error = NULL;
 	SoupBuffer *buffer;
 
+	if (!io->acked_content_sniff_decision) {
+		/* The content sniffer feature decides whether a
+		 * message needs to be sniffed while handling
+		 * got-headers, but the message may be paused in a
+		 * user handler, so we need to make sure the signal is
+		 * emitted, or delay_got_chunks is correctly setup
+		 * here.
+		 */
+		if (priv->should_sniff_content)
+			io->delay_got_chunks = TRUE;
+		else if (priv->sniffer)
+			soup_message_content_sniffed(msg, NULL);
+		io->acked_content_sniff_decision = TRUE;
+	}
+
 	while (read_to_eof || io->read_length > 0) {
 		if (priv->chunk_allocator) {
 			buffer = priv->chunk_allocator (msg, io->read_length, priv->chunk_allocator_data);
@@ -324,10 +381,25 @@ 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 = soup_message_body_new ();
+
+				soup_message_body_append_buffer (io->delayed_chunk_data, buffer);
+				io->delayed_chunk_length += buffer->length;
+
+				/* We already have enough data to perform sniffing, so do it */
+				if (io->delayed_chunk_length > priv->bytes_for_sniffing) {
+					if (!io_sniff_content (msg))
+						return 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;
 		}
 
@@ -675,6 +747,23 @@ io_read (SoupSocket *sock, SoupMessage *msg)
 	guint status;
 
  read_more:
+	/* We have delayed chunks, but are no longer delaying, so this
+	 * means we already sniffed but the message got paused while
+	 * content-sniffed was being handled, in which case we did not
+	 * emit the necessary got-chunk; See also the handling for
+	 * state SOUP_MESSAGE_IO_STATE_BODY in the switch bellow.
+	 */
+	if (io->delayed_chunk_data && !io->delay_got_chunks) {
+		SoupBuffer *sniffed_buffer = soup_message_body_flatten (io->delayed_chunk_data);
+
+		SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+		soup_message_got_chunk (msg, sniffed_buffer);
+		soup_buffer_free (sniffed_buffer);
+		soup_message_body_free (io->delayed_chunk_data);
+		io->delayed_chunk_data = NULL;
+		SOUP_MESSAGE_IO_RETURN_IF_CANCELLED_OR_PAUSED;
+	}
+
 	switch (io->read_state) {
 	case SOUP_MESSAGE_IO_STATE_NOT_STARTED:
 		return;
@@ -782,6 +871,38 @@ 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, if we haven't done it yet, of course.
+		 */
+		if (io->delayed_chunk_data) {
+			if (io->delay_got_chunks) {
+				if (!io_sniff_content (msg))
+					return;
+			} else {
+				SoupBuffer *sniffed_buffer = soup_message_body_flatten (io->delayed_chunk_data);
+
+				SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+				soup_message_got_chunk (msg, sniffed_buffer);
+				soup_buffer_free (sniffed_buffer);
+				soup_message_body_free (io->delayed_chunk_data);
+				io->delayed_chunk_data = NULL;
+
+				/* If we end up returning, read_state
+				 * needs to be set to IO_STATE_BODY,
+				 * and read_length must be 0; since we
+				 * may be coming from STATE_TRAILERS,
+				 * or may be doing a read-to-eof, we
+				 * sanitize these here. */
+				io->read_state = SOUP_MESSAGE_IO_STATE_BODY;
+				io->read_length = 0;
+				SOUP_MESSAGE_IO_RETURN_IF_CANCELLED_OR_PAUSED;
+			}
+		}
+
 		io->read_state = SOUP_MESSAGE_IO_STATE_FINISHING;
 
 		SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
@@ -885,6 +1006,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..999c335 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,
@@ -29,6 +30,10 @@ typedef struct {
 	guint              msg_flags;
 	gboolean           server_side;
 
+	SoupContentSniffer *sniffer;
+	gboolean           should_sniff_content;
+	gsize              bytes_for_sniffing;
+
 	SoupHTTPVersion    http_version, orig_http_version;
 
 	SoupURI           *uri;
diff --git a/libsoup/soup-message.c b/libsoup/soup-message.c
index 5475bb7..564227b 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,
@@ -402,6 +403,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 +895,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
+ * will not be performed, due to the sniffer deciding to trust the
+ * Content-Type sent by the server, this signal is emitted immediately
+ * after %got_headers, with %NULL as @content_type.
+ **/
+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>
