From 7652a2986b0d0ad55b2776685130f1c68d7108c7 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Thu, 6 Aug 2015 22:10:25 +0200
Subject: [PATCH] client/Response: new Client wrapper class for writing
 responses

---
 Makefile.am                      |   1 +
 src/PlaylistPrint.cxx            |  49 ++++----
 src/PlaylistPrint.hxx            |  25 +++--
 src/SongPrint.cxx                |  87 +++++++-------
 src/SongPrint.hxx                |  15 ++-
 src/Stats.cxx                    |  43 ++++---
 src/Stats.hxx                    |   5 +-
 src/TagPrint.cxx                 |  30 ++---
 src/TagPrint.hxx                 |  13 ++-
 src/TimePrint.cxx                |   6 +-
 src/TimePrint.hxx                |   4 +-
 src/client/Response.cxx          |  72 ++++++++++++
 src/client/Response.hxx          |  49 ++++++++
 src/command/AllCommands.cxx      |  65 +++++++----
 src/command/CommandError.cxx     |  55 +++++----
 src/command/CommandError.hxx     |   6 +-
 src/command/DatabaseCommands.cxx |  82 +++++++++-----
 src/command/FileCommands.cxx     |  72 ++++++------
 src/command/MessageCommands.cxx  |  34 +++---
 src/command/NeighborCommands.cxx |  16 +--
 src/command/OtherCommands.cxx    | 117 ++++++++++---------
 src/command/OutputCommands.cxx   |  29 ++---
 src/command/PlayerCommands.cxx   | 187 +++++++++++++++++--------------
 src/command/PlaylistCommands.cxx |  75 ++++++++-----
 src/command/QueueCommands.cxx    | 132 +++++++++++++---------
 src/command/Request.hxx          |  14 +--
 src/command/StickerCommands.cxx  |  69 ++++++------
 src/command/StorageCommands.cxx  |  76 +++++++------
 src/command/StorageCommands.hxx  |   5 +-
 src/command/TagCommands.cxx      |  19 ++--
 src/db/Count.cxx                 |  27 +++--
 src/db/Count.hxx                 |   7 +-
 src/db/DatabasePrint.cxx         |  97 ++++++++--------
 src/db/DatabasePrint.hxx         |  11 +-
 src/decoder/DecoderPrint.cxx     |  14 +--
 src/decoder/DecoderPrint.hxx     |   4 +-
 src/ls.cxx                       |   7 +-
 src/ls.hxx                       |   4 +-
 src/output/OutputPrint.cxx       |  13 +--
 src/output/OutputPrint.hxx       |   4 +-
 src/playlist/Print.cxx           |  21 ++--
 src/playlist/Print.hxx           |   9 +-
 src/protocol/ArgParser.cxx       |  65 +++++------
 src/protocol/ArgParser.hxx       |  22 ++--
 src/queue/QueuePrint.cxx         |  36 +++---
 src/queue/QueuePrint.hxx         |  13 ++-
 src/sticker/StickerPrint.cxx     |  14 +--
 src/sticker/StickerPrint.hxx     |   6 +-
 test/test_protocol.cxx           |  22 ++--
 49 files changed, 1068 insertions(+), 780 deletions(-)
 create mode 100644 src/client/Response.cxx
 create mode 100644 src/client/Response.hxx

diff --git a/Makefile.am b/Makefile.am
index 1889d26dd..6e56b0a6b 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -122,6 +122,7 @@ libmpd_a_SOURCES = \
 	src/client/ClientMessage.cxx src/client/ClientMessage.hxx \
 	src/client/ClientSubscribe.cxx \
 	src/client/ClientFile.cxx \
+	src/client/Response.cxx src/client/Response.hxx \
 	src/Listen.cxx src/Listen.hxx \
 	src/LogInit.cxx src/LogInit.hxx \
 	src/LogBackend.cxx src/LogBackend.hxx \
diff --git a/src/PlaylistPrint.cxx b/src/PlaylistPrint.cxx
index 069fbf430..ee3794bb9 100644
--- a/src/PlaylistPrint.cxx
+++ b/src/PlaylistPrint.cxx
@@ -27,6 +27,7 @@
 #include "Instance.hxx"
 #include "db/Interface.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "input/InputStream.hxx"
 #include "DetachedSong.hxx"
 #include "fs/Traits.hxx"
@@ -37,15 +38,17 @@
 #define SONG_TIME "Time: "
 
 void
-playlist_print_uris(Client &client, const playlist &playlist)
+playlist_print_uris(Response &r, Partition &partition,
+		    const playlist &playlist)
 {
 	const Queue &queue = playlist.queue;
 
-	queue_print_uris(client, queue, 0, queue.GetLength());
+	queue_print_uris(r, partition, queue, 0, queue.GetLength());
 }
 
 bool
-playlist_print_info(Client &client, const playlist &playlist,
+playlist_print_info(Response &r, Partition &partition,
+		    const playlist &playlist,
 		    unsigned start, unsigned end)
 {
 	const Queue &queue = playlist.queue;
@@ -58,12 +61,12 @@ playlist_print_info(Client &client, const playlist &playlist,
 		/* an invalid "start" offset is fatal */
 		return false;
 
-	queue_print_info(client, queue, start, end);
+	queue_print_info(r, partition, queue, start, end);
 	return true;
 }
 
 bool
-playlist_print_id(Client &client, const playlist &playlist,
+playlist_print_id(Response &r, Partition &partition, const playlist &playlist,
 		  unsigned id)
 {
 	int position;
@@ -73,50 +76,53 @@ playlist_print_id(Client &client, const playlist &playlist,
 		/* no such song */
 		return false;
 
-	return playlist_print_info(client, playlist, position, position + 1);
+	return playlist_print_info(r, partition,
+				   playlist, position, position + 1);
 }
 
 bool
-playlist_print_current(Client &client, const playlist &playlist)
+playlist_print_current(Response &r, Partition &partition,
+		       const playlist &playlist)
 {
 	int current_position = playlist.GetCurrentPosition();
 	if (current_position < 0)
 		return false;
 
-	queue_print_info(client, playlist.queue,
+	queue_print_info(r, partition, playlist.queue,
 			 current_position, current_position + 1);
 	return true;
 }
 
 void
-playlist_print_find(Client &client, const playlist &playlist,
+playlist_print_find(Response &r, Partition &partition,
+		    const playlist &playlist,
 		    const SongFilter &filter)
 {
-	queue_find(client, playlist.queue, filter);
+	queue_find(r, partition, playlist.queue, filter);
 }
 
 void
-playlist_print_changes_info(Client &client,
+playlist_print_changes_info(Response &r, Partition &partition,
 			    const playlist &playlist,
 			    uint32_t version)
 {
-	queue_print_changes_info(client, playlist.queue, version);
+	queue_print_changes_info(r, partition, playlist.queue, version);
 }
 
 void
-playlist_print_changes_position(Client &client,
+playlist_print_changes_position(Response &r,
 				const playlist &playlist,
 				uint32_t version)
 {
-	queue_print_changes_position(client, playlist.queue, version);
+	queue_print_changes_position(r, playlist.queue, version);
 }
 
 #ifdef ENABLE_DATABASE
 
 static bool
-PrintSongDetails(Client &client, const char *uri_utf8)
+PrintSongDetails(Response &r, Partition &partition, const char *uri_utf8)
 {
-	const Database *db = client.partition.instance.database;
+	const Database *db = partition.instance.database;
 	if (db == nullptr)
 		return false;
 
@@ -124,7 +130,7 @@ PrintSongDetails(Client &client, const char *uri_utf8)
 	if (song == nullptr)
 		return false;
 
-	song_print_info(client, *song);
+	song_print_info(r, partition, *song);
 	db->ReturnSong(song);
 	return true;
 }
@@ -132,7 +138,8 @@ PrintSongDetails(Client &client, const char *uri_utf8)
 #endif
 
 bool
-spl_print(Client &client, const char *name_utf8, bool detail,
+spl_print(Response &r, Partition &partition,
+	  const char *name_utf8, bool detail,
 	  Error &error)
 {
 #ifndef ENABLE_DATABASE
@@ -145,10 +152,10 @@ spl_print(Client &client, const char *name_utf8, bool detail,
 
 	for (const auto &uri_utf8 : contents) {
 #ifdef ENABLE_DATABASE
-		if (!detail || !PrintSongDetails(client, uri_utf8.c_str()))
+		if (!detail || !PrintSongDetails(r, partition,
+						 uri_utf8.c_str()))
 #endif
-			client_printf(client, SONG_FILE "%s\n",
-				      uri_utf8.c_str());
+			r.Format(SONG_FILE "%s\n", uri_utf8.c_str());
 	}
 
 	return true;
diff --git a/src/PlaylistPrint.hxx b/src/PlaylistPrint.hxx
index e0fcc2c2d..bc4c2cb47 100644
--- a/src/PlaylistPrint.hxx
+++ b/src/PlaylistPrint.hxx
@@ -23,15 +23,18 @@
 #include <stdint.h>
 
 struct playlist;
+struct Partition;
 class SongFilter;
 class Client;
+class Response;
 class Error;
 
 /**
  * Sends the whole playlist to the client, song URIs only.
  */
 void
-playlist_print_uris(Client &client, const playlist &playlist);
+playlist_print_uris(Response &r, Partition &partition,
+		    const playlist &playlist);
 
 /**
  * Sends a range of the playlist to the client, including all known
@@ -40,7 +43,8 @@ playlist_print_uris(Client &client, const playlist &playlist);
  * This function however fails when the start offset is invalid.
  */
 bool
-playlist_print_info(Client &client, const playlist &playlist,
+playlist_print_info(Response &r, Partition &partition,
+		    const playlist &playlist,
 		    unsigned start, unsigned end);
 
 /**
@@ -49,8 +53,8 @@ playlist_print_info(Client &client, const playlist &playlist,
  * @return true on suite, false if there is no such song
  */
 bool
-playlist_print_id(Client &client, const playlist &playlist,
-		  unsigned id);
+playlist_print_id(Response &r, Partition &partition,
+		  const playlist &playlist, unsigned id);
 
 /**
  * Sends the current song to the client.
@@ -58,20 +62,22 @@ playlist_print_id(Client &client, const playlist &playlist,
  * @return true on success, false if there is no current song
  */
 bool
-playlist_print_current(Client &client, const playlist &playlist);
+playlist_print_current(Response &r, Partition &partition,
+		       const playlist &playlist);
 
 /**
  * Find songs in the playlist.
  */
 void
-playlist_print_find(Client &client, const playlist &playlist,
+playlist_print_find(Response &r, Partition &partition,
+		    const playlist &playlist,
 		    const SongFilter &filter);
 
 /**
  * Print detailed changes since the specified playlist version.
  */
 void
-playlist_print_changes_info(Client &client,
+playlist_print_changes_info(Response &r, Partition &partition,
 			    const playlist &playlist,
 			    uint32_t version);
 
@@ -79,7 +85,7 @@ playlist_print_changes_info(Client &client,
  * Print changes since the specified playlist version, position only.
  */
 void
-playlist_print_changes_position(Client &client,
+playlist_print_changes_position(Response &r,
 				const playlist &playlist,
 				uint32_t version);
 
@@ -92,7 +98,8 @@ playlist_print_changes_position(Client &client,
  * @return true on success, false if the playlist does not exist
  */
 bool
-spl_print(Client &client, const char *name_utf8, bool detail,
+spl_print(Response &r, Partition &partition,
+	  const char *name_utf8, bool detail,
 	  Error &error);
 
 #endif
diff --git a/src/SongPrint.cxx b/src/SongPrint.cxx
index 9d10d690a..804920f9b 100644
--- a/src/SongPrint.cxx
+++ b/src/SongPrint.cxx
@@ -20,18 +20,20 @@
 #include "config.h"
 #include "SongPrint.hxx"
 #include "db/LightSong.hxx"
+#include "Partition.hxx"
+#include "Instance.hxx"
 #include "storage/StorageInterface.hxx"
 #include "DetachedSong.hxx"
 #include "TimePrint.hxx"
 #include "TagPrint.hxx"
-#include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "fs/Traits.hxx"
 #include "util/UriUtil.hxx"
 
 #define SONG_FILE "file: "
 
 static void
-song_print_uri(Client &client, const char *uri, bool base)
+song_print_uri(Response &r, Partition &partition, const char *uri, bool base)
 {
 	std::string allocated;
 
@@ -39,12 +41,14 @@ song_print_uri(Client &client, const char *uri, bool base)
 		uri = PathTraitsUTF8::GetBase(uri);
 	} else {
 #ifdef ENABLE_DATABASE
-		const Storage *storage = client.GetStorage();
+		const Storage *storage = partition.instance.storage;
 		if (storage != nullptr) {
 			const char *suffix = storage->MapToRelativeUTF8(uri);
 			if (suffix != nullptr)
 				uri = suffix;
 		}
+#else
+		(void)partition;
 #endif
 
 		allocated = uri_remove_auth(uri);
@@ -52,78 +56,81 @@ song_print_uri(Client &client, const char *uri, bool base)
 			uri = allocated.c_str();
 	}
 
-	client_printf(client, SONG_FILE "%s\n", uri);
+	r.Format(SONG_FILE "%s\n", uri);
 }
 
 void
-song_print_uri(Client &client, const LightSong &song, bool base)
+song_print_uri(Response &r, Partition &partition,
+	       const LightSong &song, bool base)
 {
-	if (!base && song.directory != nullptr) {
-		client_printf(client, SONG_FILE "%s/%s\n",
-			      song.directory, song.uri);
-	} else
-		song_print_uri(client, song.uri, base);
+	if (!base && song.directory != nullptr)
+		r.Format(SONG_FILE "%s/%s\n", song.directory, song.uri);
+	else
+		song_print_uri(r, partition, song.uri, base);
 }
 
 void
-song_print_uri(Client &client, const DetachedSong &song, bool base)
+song_print_uri(Response &r, Partition &partition,
+	       const DetachedSong &song, bool base)
 {
-	song_print_uri(client, song.GetURI(), base);
+	song_print_uri(r, partition, song.GetURI(), base);
 }
 
 void
-song_print_info(Client &client, const LightSong &song, bool base)
+song_print_info(Response &r, Partition &partition,
+		const LightSong &song, bool base)
 {
-	song_print_uri(client, song, base);
+	song_print_uri(r, partition, song, base);
 
 	const unsigned start_ms = song.start_time.ToMS();
 	const unsigned end_ms = song.end_time.ToMS();
 
 	if (end_ms > 0)
-		client_printf(client, "Range: %u.%03u-%u.%03u\n",
-			      start_ms / 1000,
-			      start_ms % 1000,
-			      end_ms / 1000,
-			      end_ms % 1000);
+		r.Format("Range: %u.%03u-%u.%03u\n",
+			 start_ms / 1000,
+			 start_ms % 1000,
+			 end_ms / 1000,
+			 end_ms % 1000);
 	else if (start_ms > 0)
-		client_printf(client, "Range: %u.%03u-\n",
-			      start_ms / 1000,
-			      start_ms % 1000);
+		r.Format("Range: %u.%03u-\n",
+			 start_ms / 1000,
+			 start_ms % 1000);
 
 	if (song.mtime > 0)
-		time_print(client, "Last-Modified", song.mtime);
+		time_print(r, "Last-Modified", song.mtime);
 
-	tag_print(client, *song.tag);
+	tag_print(r, *song.tag);
 }
 
 void
-song_print_info(Client &client, const DetachedSong &song, bool base)
+song_print_info(Response &r, Partition &partition,
+		const DetachedSong &song, bool base)
 {
-	song_print_uri(client, song, base);
+	song_print_uri(r, partition, song, base);
 
 	const unsigned start_ms = song.GetStartTime().ToMS();
 	const unsigned end_ms = song.GetEndTime().ToMS();
 
 	if (end_ms > 0)
-		client_printf(client, "Range: %u.%03u-%u.%03u\n",
-			      start_ms / 1000,
-			      start_ms % 1000,
-			      end_ms / 1000,
-			      end_ms % 1000);
+		r.Format("Range: %u.%03u-%u.%03u\n",
+			 start_ms / 1000,
+			 start_ms % 1000,
+			 end_ms / 1000,
+			 end_ms % 1000);
 	else if (start_ms > 0)
-		client_printf(client, "Range: %u.%03u-\n",
-			      start_ms / 1000,
-			      start_ms % 1000);
+		r.Format("Range: %u.%03u-\n",
+			 start_ms / 1000,
+			 start_ms % 1000);
 
 	if (song.GetLastModified() > 0)
-		time_print(client, "Last-Modified", song.GetLastModified());
+		time_print(r, "Last-Modified", song.GetLastModified());
 
-	tag_print_values(client, song.GetTag());
+	tag_print_values(r, song.GetTag());
 
 	const auto duration = song.GetDuration();
 	if (!duration.IsNegative())
-		client_printf(client, "Time: %i\n"
-			      "duration: %1.3f\n",
-			      duration.RoundS(),
-			      duration.ToDoubleS());
+		r.Format("Time: %i\n"
+			 "duration: %1.3f\n",
+			 duration.RoundS(),
+			 duration.ToDoubleS());
 }
diff --git a/src/SongPrint.hxx b/src/SongPrint.hxx
index 5b9a507ac..50be70fa8 100644
--- a/src/SongPrint.hxx
+++ b/src/SongPrint.hxx
@@ -22,18 +22,23 @@
 
 struct LightSong;
 class DetachedSong;
-class Client;
+class Response;
+struct Partition;
 
 void
-song_print_info(Client &client, const DetachedSong &song, bool base=false);
+song_print_info(Response &r, Partition &partition,
+		const DetachedSong &song, bool base=false);
 
 void
-song_print_info(Client &client, const LightSong &song, bool base=false);
+song_print_info(Response &r, Partition &partition,
+		const LightSong &song, bool base=false);
 
 void
-song_print_uri(Client &client, const LightSong &song, bool base=false);
+song_print_uri(Response &r, Partition &partition,
+	       const LightSong &song, bool base=false);
 
 void
-song_print_uri(Client &client, const DetachedSong &song, bool base=false);
+song_print_uri(Response &r, Partition &partition,
+	       const DetachedSong &song, bool base=false);
 
 #endif
diff --git a/src/Stats.cxx b/src/Stats.cxx
index cb33ecad3..9ed3a25dd 100644
--- a/src/Stats.cxx
+++ b/src/Stats.cxx
@@ -20,7 +20,7 @@
 #include "config.h"
 #include "Stats.hxx"
 #include "PlayerControl.hxx"
-#include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "Partition.hxx"
 #include "Instance.hxx"
 #include "db/Selection.hxx"
@@ -94,7 +94,7 @@ stats_update(const Database &db)
 }
 
 static void
-db_stats_print(Client &client, const Database &db)
+db_stats_print(Response &r, const Database &db)
 {
 	if (!stats_update(db))
 		return;
@@ -102,41 +102,38 @@ db_stats_print(Client &client, const Database &db)
 	unsigned total_duration_s =
 		std::chrono::duration_cast<std::chrono::seconds>(stats.total_duration).count();
 
-	client_printf(client,
-		      "artists: %u\n"
-		      "albums: %u\n"
-		      "songs: %u\n"
-		      "db_playtime: %u\n",
-		      stats.artist_count,
-		      stats.album_count,
-		      stats.song_count,
-		      total_duration_s);
+	r.Format("artists: %u\n"
+		 "albums: %u\n"
+		 "songs: %u\n"
+		 "db_playtime: %u\n",
+		 stats.artist_count,
+		 stats.album_count,
+		 stats.song_count,
+		 total_duration_s);
 
 	const time_t update_stamp = db.GetUpdateStamp();
 	if (update_stamp > 0)
-		client_printf(client,
-			      "db_update: %lu\n",
-			      (unsigned long)update_stamp);
+		r.Format("db_update: %lu\n",
+			 (unsigned long)update_stamp);
 }
 
 #endif
 
 void
-stats_print(Client &client)
+stats_print(Response &r, const Partition &partition)
 {
-	client_printf(client,
-		      "uptime: %u\n"
-		      "playtime: %lu\n",
+	r.Format("uptime: %u\n"
+		 "playtime: %lu\n",
 #ifdef WIN32
-		      GetProcessUptimeS(),
+		 GetProcessUptimeS(),
 #else
-		      MonotonicClockS() - start_time,
+		 MonotonicClockS() - start_time,
 #endif
-		      (unsigned long)(client.player_control.GetTotalPlayTime() + 0.5));
+		 (unsigned long)(partition.pc.GetTotalPlayTime() + 0.5));
 
 #ifdef ENABLE_DATABASE
-	const Database *db = client.partition.instance.database;
+	const Database *db = partition.instance.database;
 	if (db != nullptr)
-		db_stats_print(client, *db);
+		db_stats_print(r, *db);
 #endif
 }
diff --git a/src/Stats.hxx b/src/Stats.hxx
index c3f56286e..879e1b7be 100644
--- a/src/Stats.hxx
+++ b/src/Stats.hxx
@@ -20,7 +20,8 @@
 #ifndef MPD_STATS_HXX
 #define MPD_STATS_HXX
 
-class Client;
+class Response;
+struct Partition;
 
 void
 stats_global_init();
@@ -29,6 +30,6 @@ void
 stats_invalidate();
 
 void
-stats_print(Client &client);
+stats_print(Response &r, const Partition &partition);
 
 #endif
diff --git a/src/TagPrint.cxx b/src/TagPrint.cxx
index 39027ba11..331cabda5 100644
--- a/src/TagPrint.cxx
+++ b/src/TagPrint.cxx
@@ -21,40 +21,40 @@
 #include "TagPrint.hxx"
 #include "tag/Tag.hxx"
 #include "tag/TagSettings.h"
-#include "client/Client.hxx"
+#include "client/Response.hxx"
 
-void tag_print_types(Client &client)
+void
+tag_print_types(Response &r)
 {
 	int i;
 
 	for (i = 0; i < TAG_NUM_OF_ITEM_TYPES; i++) {
 		if (!ignore_tag_items[i])
-			client_printf(client, "tagtype: %s\n",
-				      tag_item_names[i]);
+			r.Format("tagtype: %s\n", tag_item_names[i]);
 	}
 }
 
 void
-tag_print(Client &client, TagType type, const char *value)
+tag_print(Response &r, TagType type, const char *value)
 {
-	client_printf(client, "%s: %s\n", tag_item_names[type], value);
+	r.Format("%s: %s\n", tag_item_names[type], value);
 }
 
 void
-tag_print_values(Client &client, const Tag &tag)
+tag_print_values(Response &r, const Tag &tag)
 {
 	for (const auto &i : tag)
-		client_printf(client, "%s: %s\n",
-			      tag_item_names[i.type], i.value);
+		r.Format("%s: %s\n", tag_item_names[i.type], i.value);
 }
 
-void tag_print(Client &client, const Tag &tag)
+void
+tag_print(Response &r, const Tag &tag)
 {
 	if (!tag.duration.IsNegative())
-		client_printf(client, "Time: %i\n"
-			      "duration: %1.3f\n",
-			      tag.duration.RoundS(),
-			      tag.duration.ToDoubleS());
+		r.Format("Time: %i\n"
+			 "duration: %1.3f\n",
+			 tag.duration.RoundS(),
+			 tag.duration.ToDoubleS());
 
-	tag_print_values(client, tag);
+	tag_print_values(r, tag);
 }
diff --git a/src/TagPrint.hxx b/src/TagPrint.hxx
index 70cb6323a..30405638e 100644
--- a/src/TagPrint.hxx
+++ b/src/TagPrint.hxx
@@ -25,17 +25,18 @@
 enum TagType : uint8_t;
 
 struct Tag;
-class Client;
-
-void tag_print_types(Client &client);
+class Response;
 
 void
-tag_print(Client &client, TagType type, const char *value);
+tag_print_types(Response &response);
 
 void
-tag_print_values(Client &client, const Tag &tag);
+tag_print(Response &response, TagType type, const char *value);
 
 void
-tag_print(Client &client, const Tag &tag);
+tag_print_values(Response &response, const Tag &tag);
+
+void
+tag_print(Response &response, const Tag &tag);
 
 #endif
diff --git a/src/TimePrint.cxx b/src/TimePrint.cxx
index 326743869..f9a4dbd94 100644
--- a/src/TimePrint.cxx
+++ b/src/TimePrint.cxx
@@ -19,10 +19,10 @@
 
 #include "config.h"
 #include "TimePrint.hxx"
-#include "client/Client.hxx"
+#include "client/Response.hxx"
 
 void
-time_print(Client &client, const char *name, time_t t)
+time_print(Response &r, const char *name, time_t t)
 {
 #ifdef WIN32
 	const struct tm *tm2 = gmtime(&t);
@@ -41,5 +41,5 @@ time_print(Client &client, const char *name, time_t t)
 		 "%FT%TZ",
 #endif
 		 tm2);
-	client_printf(client, "%s: %s\n", name, buffer);
+	r.Format("%s: %s\n", name, buffer);
 }
diff --git a/src/TimePrint.hxx b/src/TimePrint.hxx
index 8f1f21050..6ded9cca0 100644
--- a/src/TimePrint.hxx
+++ b/src/TimePrint.hxx
@@ -22,12 +22,12 @@
 
 #include <time.h>
 
-class Client;
+class Response;
 
 /**
  * Write a line with a time stamp to the client.
  */
 void
-time_print(Client &client, const char *name, time_t t);
+time_print(Response &r, const char *name, time_t t);
 
 #endif
diff --git a/src/client/Response.cxx b/src/client/Response.cxx
new file mode 100644
index 000000000..9beaab156
--- /dev/null
+++ b/src/client/Response.cxx
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2003-2015 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "Response.hxx"
+#include "Client.hxx"
+#include "protocol/Result.hxx"
+#include "util/FormatString.hxx"
+
+#include <string.h>
+
+bool
+Response::Write(const void *data, size_t length)
+{
+	return client.Write(data, length);
+}
+
+bool
+Response::Write(const char *data)
+{
+	return Write(data, strlen(data));
+}
+
+bool
+Response::FormatV(const char *fmt, va_list args)
+{
+	char *p = FormatNewV(fmt, args);
+	bool success = Write(p);
+	delete[] p;
+	return success;
+}
+
+bool
+Response::Format(const char *fmt, ...)
+{
+	va_list args;
+	va_start(args, fmt);
+	bool success = FormatV(fmt, args);
+	va_end(args);
+	return success;
+}
+
+void
+Response::Error(enum ack code, const char *msg)
+{
+	command_error(client, code, "%s", msg);
+}
+
+void
+Response::FormatError(enum ack code, const char *fmt, ...)
+{
+	va_list args;
+	va_start(args, fmt);
+	command_error_v(client, code, fmt, args);
+	va_end(args);
+}
diff --git a/src/client/Response.hxx b/src/client/Response.hxx
new file mode 100644
index 000000000..ba53298c8
--- /dev/null
+++ b/src/client/Response.hxx
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2003-2015 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_RESPONSE_HXX
+#define MPD_RESPONSE_HXX
+
+#include "check.h"
+#include "protocol/Ack.hxx"
+
+#include <stddef.h>
+#include <stdarg.h>
+
+class Client;
+
+class Response {
+	Client &client;
+
+public:
+	explicit Response(Client &_client):client(_client) {}
+
+	Response(const Response &) = delete;
+	Response &operator=(const Response &) = delete;
+
+	bool Write(const void *data, size_t length);
+	bool Write(const char *data);
+	bool FormatV(const char *fmt, va_list args);
+	bool Format(const char *fmt, ...);
+
+	void Error(enum ack code, const char *msg);
+	void FormatError(enum ack code, const char *fmt, ...);
+};
+
+#endif
diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx
index 9f0b0a606..57d4db983 100644
--- a/src/command/AllCommands.cxx
+++ b/src/command/AllCommands.cxx
@@ -36,6 +36,7 @@
 #include "protocol/Result.hxx"
 #include "Partition.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "util/Macros.hxx"
 #include "util/Tokenizer.hxx"
 #include "util/Error.hxx"
@@ -226,38 +227,50 @@ command_available(gcc_unused const Partition &partition,
 	return true;
 }
 
-/* don't be fooled, this is the command handler for "commands" command */
 static CommandResult
-handle_commands(Client &client, gcc_unused Request args)
+PrintAvailableCommands(Response &r, const Partition &partition,
+		     unsigned permission)
 {
-	const unsigned permission = client.GetPermission();
-
 	for (unsigned i = 0; i < num_commands; ++i) {
 		const struct command *cmd = &commands[i];
 
 		if (cmd->permission == (permission & cmd->permission) &&
-		    command_available(client.partition, cmd))
-			client_printf(client, "command: %s\n", cmd->cmd);
+		    command_available(partition, cmd))
+			r.Format("command: %s\n", cmd->cmd);
 	}
 
 	return CommandResult::OK;
 }
 
 static CommandResult
-handle_not_commands(Client &client, gcc_unused Request args)
+PrintUnavailableCommands(Response &r, unsigned permission)
 {
-	const unsigned permission = client.GetPermission();
-
 	for (unsigned i = 0; i < num_commands; ++i) {
 		const struct command *cmd = &commands[i];
 
 		if (cmd->permission != (permission & cmd->permission))
-			client_printf(client, "command: %s\n", cmd->cmd);
+			r.Format("command: %s\n", cmd->cmd);
 	}
 
 	return CommandResult::OK;
 }
 
+/* don't be fooled, this is the command handler for "commands" command */
+static CommandResult
+handle_commands(Client &client, gcc_unused Request args)
+{
+	Response r(client);
+	return PrintAvailableCommands(r, client.partition,
+				      client.GetPermission());
+}
+
+static CommandResult
+handle_not_commands(Client &client, gcc_unused Request args)
+{
+	Response r(client);
+	return PrintUnavailableCommands(r, client.GetPermission());
+}
+
 void
 command_init()
 {
@@ -299,7 +312,8 @@ command_check_request(const struct command *cmd, Client &client,
 		      unsigned permission, Request args)
 {
 	if (cmd->permission != (permission & cmd->permission)) {
-		command_error(client, ACK_ERROR_PERMISSION,
+		Response r(client);
+		r.FormatError(ACK_ERROR_PERMISSION,
 			      "you don't have permission for \"%s\"",
 			      cmd->cmd);
 		return false;
@@ -312,16 +326,19 @@ command_check_request(const struct command *cmd, Client &client,
 		return true;
 
 	if (min == max && unsigned(max) != args.size) {
-		command_error(client, ACK_ERROR_ARG,
+		Response r(client);
+		r.FormatError(ACK_ERROR_ARG,
 			      "wrong number of arguments for \"%s\"",
 			      cmd->cmd);
 		return false;
 	} else if (args.size < unsigned(min)) {
-		command_error(client, ACK_ERROR_ARG,
+		Response r(client);
+		r.FormatError(ACK_ERROR_ARG,
 			      "too few arguments for \"%s\"", cmd->cmd);
 		return false;
 	} else if (max >= 0 && args.size > unsigned(max)) {
-		command_error(client, ACK_ERROR_ARG,
+		Response r(client);
+		r.FormatError(ACK_ERROR_ARG,
 			      "too many arguments for \"%s\"", cmd->cmd);
 		return false;
 	} else
@@ -336,7 +353,8 @@ command_checked_lookup(Client &client, unsigned permission,
 
 	const struct command *cmd = command_lookup(cmd_name);
 	if (cmd == nullptr) {
-		command_error(client, ACK_ERROR_UNKNOWN,
+		Response r(client);
+		r.FormatError(ACK_ERROR_UNKNOWN,
 			      "unknown command \"%s\"", cmd_name);
 		return nullptr;
 	}
@@ -357,7 +375,7 @@ command_process(Client &client, unsigned num, char *line)
 	command_list_num = num;
 
 	/* get the command name (first word on the line) */
-	/* we have to set current_command because command_error()
+	/* we have to set current_command because Response::Error()
 	   expects it to be set */
 
 	Tokenizer tokenizer(line);
@@ -366,12 +384,12 @@ command_process(Client &client, unsigned num, char *line)
 		tokenizer.NextWord(error);
 	if (cmd_name == nullptr) {
 		current_command = "";
+
+		Response r(client);
 		if (tokenizer.IsEnd())
-			command_error(client, ACK_ERROR_UNKNOWN,
-				      "No command given");
+			r.FormatError(ACK_ERROR_UNKNOWN, "No command given");
 		else
-			command_error(client, ACK_ERROR_UNKNOWN,
-				      "%s", error.GetMessage());
+			r.Error(ACK_ERROR_UNKNOWN, error.GetMessage());
 
 		current_command = nullptr;
 
@@ -387,8 +405,8 @@ command_process(Client &client, unsigned num, char *line)
 
 	while (true) {
 		if (args.size == COMMAND_ARGV_MAX) {
-			command_error(client, ACK_ERROR_ARG,
-				      "Too many arguments");
+			Response r(client);
+			r.Error(ACK_ERROR_ARG, "Too many arguments");
 			current_command = nullptr;
 			return CommandResult::ERROR;
 		}
@@ -398,7 +416,8 @@ command_process(Client &client, unsigned num, char *line)
 			if (tokenizer.IsEnd())
 				break;
 
-			command_error(client, ACK_ERROR_ARG, "%s", error.GetMessage());
+			Response r(client);
+			r.Error(ACK_ERROR_UNKNOWN, error.GetMessage());
 			current_command = nullptr;
 			return CommandResult::ERROR;
 		}
diff --git a/src/command/CommandError.cxx b/src/command/CommandError.cxx
index da0571295..d95722c3b 100644
--- a/src/command/CommandError.cxx
+++ b/src/command/CommandError.cxx
@@ -20,7 +20,7 @@
 #include "config.h"
 #include "CommandError.hxx"
 #include "db/DatabaseError.hxx"
-#include "protocol/Result.hxx"
+#include "client/Response.hxx"
 #include "util/Error.hxx"
 #include "Log.hxx"
 
@@ -29,57 +29,55 @@
 #include <errno.h>
 
 CommandResult
-print_playlist_result(Client &client, PlaylistResult result)
+print_playlist_result(Response &r, PlaylistResult result)
 {
 	switch (result) {
 	case PlaylistResult::SUCCESS:
 		return CommandResult::OK;
 
 	case PlaylistResult::ERRNO:
-		command_error(client, ACK_ERROR_SYSTEM, "%s",
-			      strerror(errno));
+		r.Error(ACK_ERROR_SYSTEM, strerror(errno));
 		return CommandResult::ERROR;
 
 	case PlaylistResult::DENIED:
-		command_error(client, ACK_ERROR_PERMISSION, "Access denied");
+		r.Error(ACK_ERROR_PERMISSION, "Access denied");
 		return CommandResult::ERROR;
 
 	case PlaylistResult::NO_SUCH_SONG:
-		command_error(client, ACK_ERROR_NO_EXIST, "No such song");
+		r.Error(ACK_ERROR_NO_EXIST, "No such song");
 		return CommandResult::ERROR;
 
 	case PlaylistResult::NO_SUCH_LIST:
-		command_error(client, ACK_ERROR_NO_EXIST, "No such playlist");
+		r.Error(ACK_ERROR_NO_EXIST, "No such playlist");
 		return CommandResult::ERROR;
 
 	case PlaylistResult::LIST_EXISTS:
-		command_error(client, ACK_ERROR_EXIST,
-			      "Playlist already exists");
+		r.Error(ACK_ERROR_EXIST, "Playlist already exists");
 		return CommandResult::ERROR;
 
 	case PlaylistResult::BAD_NAME:
-		command_error(client, ACK_ERROR_ARG,
-			      "playlist name is invalid: "
-			      "playlist names may not contain slashes,"
-			      " newlines or carriage returns");
+		r.Error(ACK_ERROR_ARG,
+			"playlist name is invalid: "
+			"playlist names may not contain slashes,"
+			" newlines or carriage returns");
 		return CommandResult::ERROR;
 
 	case PlaylistResult::BAD_RANGE:
-		command_error(client, ACK_ERROR_ARG, "Bad song index");
+		r.Error(ACK_ERROR_ARG, "Bad song index");
 		return CommandResult::ERROR;
 
 	case PlaylistResult::NOT_PLAYING:
-		command_error(client, ACK_ERROR_PLAYER_SYNC, "Not playing");
+		r.Error(ACK_ERROR_PLAYER_SYNC, "Not playing");
 		return CommandResult::ERROR;
 
 	case PlaylistResult::TOO_LARGE:
-		command_error(client, ACK_ERROR_PLAYLIST_MAX,
-			      "playlist is at the max size");
+		r.Error(ACK_ERROR_PLAYLIST_MAX,
+			"playlist is at the max size");
 		return CommandResult::ERROR;
 
 	case PlaylistResult::DISABLED:
-		command_error(client, ACK_ERROR_UNKNOWN,
-			      "stored playlist support is disabled");
+		r.Error(ACK_ERROR_UNKNOWN,
+			"stored playlist support is disabled");
 		return CommandResult::ERROR;
 	}
 
@@ -88,42 +86,39 @@ print_playlist_result(Client &client, PlaylistResult result)
 }
 
 CommandResult
-print_error(Client &client, const Error &error)
+print_error(Response &r, const Error &error)
 {
 	assert(error.IsDefined());
 
 	LogError(error);
 
 	if (error.IsDomain(playlist_domain)) {
-		return print_playlist_result(client,
+		return print_playlist_result(r,
 					     PlaylistResult(error.GetCode()));
 	} else if (error.IsDomain(ack_domain)) {
-		command_error(client, (ack)error.GetCode(),
-			      "%s", error.GetMessage());
+		r.Error((ack)error.GetCode(), error.GetMessage());
 		return CommandResult::ERROR;
 #ifdef ENABLE_DATABASE
 	} else if (error.IsDomain(db_domain)) {
 		switch ((enum db_error)error.GetCode()) {
 		case DB_DISABLED:
-			command_error(client, ACK_ERROR_NO_EXIST, "%s",
-				      error.GetMessage());
+			r.Error(ACK_ERROR_NO_EXIST, error.GetMessage());
 			return CommandResult::ERROR;
 
 		case DB_NOT_FOUND:
-			command_error(client, ACK_ERROR_NO_EXIST, "Not found");
+			r.Error(ACK_ERROR_NO_EXIST, "Not found");
 			return CommandResult::ERROR;
 
 		case DB_CONFLICT:
-			command_error(client, ACK_ERROR_ARG, "Conflict");
+			r.Error(ACK_ERROR_ARG, "Conflict");
 			return CommandResult::ERROR;
 		}
 #endif
 	} else if (error.IsDomain(errno_domain)) {
-		command_error(client, ACK_ERROR_SYSTEM, "%s",
-			      strerror(error.GetCode()));
+		r.Error(ACK_ERROR_SYSTEM, strerror(error.GetCode()));
 		return CommandResult::ERROR;
 	}
 
-	command_error(client, ACK_ERROR_UNKNOWN, "error");
+	r.Error(ACK_ERROR_UNKNOWN, "error");
 	return CommandResult::ERROR;
 }
diff --git a/src/command/CommandError.hxx b/src/command/CommandError.hxx
index 6c42ed960..e33386078 100644
--- a/src/command/CommandError.hxx
+++ b/src/command/CommandError.hxx
@@ -23,16 +23,16 @@
 #include "CommandResult.hxx"
 #include "PlaylistError.hxx"
 
-class Client;
+class Response;
 class Error;
 
 CommandResult
-print_playlist_result(Client &client, PlaylistResult result);
+print_playlist_result(Response &r, PlaylistResult result);
 
 /**
  * Send the #Error to the client.
  */
 CommandResult
-print_error(Client &client, const Error &error);
+print_error(Response &r, const Error &error);
 
 #endif
diff --git a/src/command/DatabaseCommands.cxx b/src/command/DatabaseCommands.cxx
index 6e6f3399b..62764139e 100644
--- a/src/command/DatabaseCommands.cxx
+++ b/src/command/DatabaseCommands.cxx
@@ -28,11 +28,11 @@
 #include "db/Selection.hxx"
 #include "CommandError.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "tag/Tag.hxx"
 #include "util/ConstBuffer.hxx"
 #include "util/Error.hxx"
 #include "SongFilter.hxx"
-#include "protocol/Result.hxx"
 #include "BulkEdit.hxx"
 
 #include <string.h>
@@ -40,11 +40,13 @@
 CommandResult
 handle_listfiles_db(Client &client, const char *uri)
 {
+	Response r(client);
 	const DatabaseSelection selection(uri, false);
 
 	Error error;
-	if (!db_selection_print(client, selection, false, true, error))
-		return print_error(client, error);
+	if (!db_selection_print(r, client.partition,
+				selection, false, true, error))
+		return print_error(r, error);
 
 	return CommandResult::OK;
 }
@@ -52,14 +54,17 @@ handle_listfiles_db(Client &client, const char *uri)
 CommandResult
 handle_lsinfo2(Client &client, Request args)
 {
+	Response r(client);
+
 	/* default is root directory */
 	const auto uri = args.GetOptional(0, "");
 
 	const DatabaseSelection selection(uri, false);
 
 	Error error;
-	if (!db_selection_print(client, selection, true, false, error))
-		return print_error(client, error);
+	if (!db_selection_print(r, client.partition,
+				selection, true, false, error))
+		return print_error(r, error);
 
 	return CommandResult::OK;
 }
@@ -67,9 +72,11 @@ handle_lsinfo2(Client &client, Request args)
 static CommandResult
 handle_match(Client &client, Request args, bool fold_case)
 {
+	Response r(client);
+
 	RangeArg window;
 	if (args.size >= 2 && strcmp(args[args.size - 2], "window") == 0) {
-		if (!args.Parse(args.size - 1, window, client))
+		if (!args.Parse(args.size - 1, window, r))
 			return CommandResult::ERROR;
 
 		args.pop_back();
@@ -79,17 +86,18 @@ handle_match(Client &client, Request args, bool fold_case)
 
 	SongFilter filter;
 	if (!filter.Parse(args, fold_case)) {
-		command_error(client, ACK_ERROR_ARG, "incorrect arguments");
+		r.Error(ACK_ERROR_ARG, "incorrect arguments");
 		return CommandResult::ERROR;
 	}
 
 	const DatabaseSelection selection("", true, &filter);
 
 	Error error;
-	return db_selection_print(client, selection, true, false,
+	return db_selection_print(r, client.partition,
+				  selection, true, false,
 				  window.start, window.end, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
@@ -107,9 +115,11 @@ handle_search(Client &client, Request args)
 static CommandResult
 handle_match_add(Client &client, Request args, bool fold_case)
 {
+	Response r(client);
+
 	SongFilter filter;
 	if (!filter.Parse(args, fold_case)) {
-		command_error(client, ACK_ERROR_ARG, "incorrect arguments");
+		r.Error(ACK_ERROR_ARG, "incorrect arguments");
 		return CommandResult::ERROR;
 	}
 
@@ -119,7 +129,7 @@ handle_match_add(Client &client, Request args, bool fold_case)
 	Error error;
 	return AddFromDatabase(client.partition, selection, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
@@ -137,34 +147,38 @@ handle_searchadd(Client &client, Request args)
 CommandResult
 handle_searchaddpl(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *playlist = args.shift();
 
 	SongFilter filter;
 	if (!filter.Parse(args, true)) {
-		command_error(client, ACK_ERROR_ARG, "incorrect arguments");
+		r.Error(ACK_ERROR_ARG, "incorrect arguments");
 		return CommandResult::ERROR;
 	}
 
 	Error error;
 	const Database *db = client.GetDatabase(error);
 	if (db == nullptr)
-		return print_error(client, error);
+		return print_error(r, error);
 
 	return search_add_to_playlist(*db, *client.GetStorage(),
 				      "", playlist, &filter, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_count(Client &client, Request args)
 {
+	Response r(client);
+
 	TagType group = TAG_NUM_OF_ITEM_TYPES;
 	if (args.size >= 2 && strcmp(args[args.size - 2], "group") == 0) {
 		const char *s = args[args.size - 1];
 		group = tag_name_parse_i(s);
 		if (group == TAG_NUM_OF_ITEM_TYPES) {
-			command_error(client, ACK_ERROR_ARG,
+			r.FormatError(ACK_ERROR_ARG,
 				      "Unknown tag type: %s", s);
 			return CommandResult::ERROR;
 		}
@@ -175,38 +189,43 @@ handle_count(Client &client, Request args)
 
 	SongFilter filter;
 	if (!args.IsEmpty() && !filter.Parse(args, false)) {
-		command_error(client, ACK_ERROR_ARG, "incorrect arguments");
+		r.Error(ACK_ERROR_ARG, "incorrect arguments");
 		return CommandResult::ERROR;
 	}
 
 	Error error;
-	return PrintSongCount(client, "", &filter, group, error)
+	return PrintSongCount(r, client.partition, "", &filter, group, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_listall(Client &client, Request args)
 {
+	Response r(client);
+
 	/* default is root directory */
 	const auto uri = args.GetOptional(0, "");
 
 	Error error;
-	return db_selection_print(client, DatabaseSelection(uri, true),
+	return db_selection_print(r, client.partition,
+				  DatabaseSelection(uri, true),
 				  false, false, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_list(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *tag_name = args.shift();
 	unsigned tagType = locate_parse_type(tag_name);
 
 	if (tagType >= TAG_NUM_OF_ITEM_TYPES &&
 	    tagType != LOCATE_TAG_FILE_TYPE) {
-		command_error(client, ACK_ERROR_ARG,
+		r.FormatError(ACK_ERROR_ARG,
 			      "Unknown tag type: %s", tag_name);
 		return CommandResult::ERROR;
 	}
@@ -217,7 +236,7 @@ handle_list(Client &client, Request args)
 	if (args.size == 1) {
 		/* for compatibility with < 0.12.0 */
 		if (tagType != TAG_ALBUM) {
-			command_error(client, ACK_ERROR_ARG,
+			r.FormatError(ACK_ERROR_ARG,
 				      "should be \"%s\" for 3 arguments",
 				      tag_item_names[TAG_ALBUM]);
 			return CommandResult::ERROR;
@@ -231,7 +250,7 @@ handle_list(Client &client, Request args)
 		const char *s = args[args.size - 1];
 		TagType gt = tag_name_parse_i(s);
 		if (gt == TAG_NUM_OF_ITEM_TYPES) {
-			command_error(client, ACK_ERROR_ARG,
+			r.FormatError(ACK_ERROR_ARG,
 				      "Unknown tag type: %s", s);
 			return CommandResult::ERROR;
 		}
@@ -246,8 +265,7 @@ handle_list(Client &client, Request args)
 		filter = new SongFilter();
 		if (!filter->Parse(args, false)) {
 			delete filter;
-			command_error(client, ACK_ERROR_ARG,
-				      "not able to parse args");
+			r.Error(ACK_ERROR_ARG, "not able to parse args");
 			return CommandResult::ERROR;
 		}
 	}
@@ -255,15 +273,16 @@ handle_list(Client &client, Request args)
 	if (tagType < TAG_NUM_OF_ITEM_TYPES &&
 	    group_mask & (1u << tagType)) {
 		delete filter;
-		command_error(client, ACK_ERROR_ARG, "Conflicting group");
+		r.Error(ACK_ERROR_ARG, "Conflicting group");
 		return CommandResult::ERROR;
 	}
 
 	Error error;
 	CommandResult ret =
-		PrintUniqueTags(client, tagType, group_mask, filter, error)
+		PrintUniqueTags(r, client.partition,
+				tagType, group_mask, filter, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 
 	delete filter;
 
@@ -273,12 +292,15 @@ handle_list(Client &client, Request args)
 CommandResult
 handle_listallinfo(Client &client, Request args)
 {
+	Response r(client);
+
 	/* default is root directory */
 	const auto uri = args.GetOptional(0, "");
 
 	Error error;
-	return db_selection_print(client, DatabaseSelection(uri, true),
+	return db_selection_print(r, client.partition,
+				  DatabaseSelection(uri, true),
 				  true, false, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
diff --git a/src/command/FileCommands.cxx b/src/command/FileCommands.cxx
index 574254d7a..c30dad8c8 100644
--- a/src/command/FileCommands.cxx
+++ b/src/command/FileCommands.cxx
@@ -24,8 +24,8 @@
 #include "Request.hxx"
 #include "CommandError.hxx"
 #include "protocol/Ack.hxx"
-#include "protocol/Result.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "util/ConstBuffer.hxx"
 #include "util/CharUtil.hxx"
 #include "util/UriUtil.hxx"
@@ -72,21 +72,22 @@ skip_path(Path name_fs)
 CommandResult
 handle_listfiles_local(Client &client, const char *path_utf8)
 {
+	Response r(client);
+
 	const auto path_fs = AllocatedPath::FromUTF8(path_utf8);
 	if (path_fs.IsNull()) {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "unsupported file name");
+		r.Error(ACK_ERROR_NO_EXIST, "unsupported file name");
 		return CommandResult::ERROR;
 	}
 
 	Error error;
 	if (!client.AllowFile(path_fs, error))
-		return print_error(client, error);
+		return print_error(r, error);
 
 	DirectoryReader reader(path_fs);
 	if (reader.HasFailed()) {
 		error.FormatErrno("Failed to open '%s'", path_utf8);
-		return print_error(client, error);
+		return print_error(r, error);
 	}
 
 	while (reader.ReadEntry()) {
@@ -105,17 +106,16 @@ handle_listfiles_local(Client &client, const char *path_utf8)
 			continue;
 
 		if (fi.IsRegular())
-			client_printf(client, "file: %s\n"
-				      "size: %" PRIu64 "\n",
-				      name_utf8.c_str(),
-				      fi.GetSize());
+			r.Format("file: %s\n"
+				 "size: %" PRIu64 "\n",
+				 name_utf8.c_str(),
+				 fi.GetSize());
 		else if (fi.IsDirectory())
-			client_printf(client, "directory: %s\n",
-				      name_utf8.c_str());
+			r.Format("directory: %s\n", name_utf8.c_str());
 		else
 			continue;
 
-		time_print(client, "Last-Modified", fi.GetModificationTime());
+		time_print(r, "Last-Modified", fi.GetModificationTime());
 	}
 
 	return CommandResult::OK;
@@ -158,10 +158,10 @@ IsValidValue(const char *p)
 static void
 print_pair(const char *key, const char *value, void *ctx)
 {
-	Client &client = *(Client *)ctx;
+	auto &r = *(Response *)ctx;
 
 	if (IsValidName(key) && IsValidValue(value))
-		client_printf(client, "%s: %s\n", key, value);
+		r.Format("%s: %s\n", key, value);
 }
 
 static constexpr tag_handler print_comment_handler = {
@@ -171,17 +171,15 @@ static constexpr tag_handler print_comment_handler = {
 };
 
 static CommandResult
-read_stream_comments(Client &client, const char *uri)
+read_stream_comments(Response &r, const char *uri)
 {
 	if (!uri_supported_scheme(uri)) {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "unsupported URI scheme");
+		r.Error(ACK_ERROR_NO_EXIST, "unsupported URI scheme");
 		return CommandResult::ERROR;
 	}
 
-	if (!tag_stream_scan(uri, print_comment_handler, &client)) {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "Failed to load file");
+	if (!tag_stream_scan(uri, print_comment_handler, &r)) {
+		r.Error(ACK_ERROR_NO_EXIST, "Failed to load file");
 		return CommandResult::ERROR;
 	}
 
@@ -190,16 +188,15 @@ read_stream_comments(Client &client, const char *uri)
 }
 
 static CommandResult
-read_file_comments(Client &client, const Path path_fs)
+read_file_comments(Response &r, const Path path_fs)
 {
-	if (!tag_file_scan(path_fs, print_comment_handler, &client)) {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "Failed to load file");
+	if (!tag_file_scan(path_fs, print_comment_handler, &r)) {
+		r.Error(ACK_ERROR_NO_EXIST, "Failed to load file");
 		return CommandResult::ERROR;
 	}
 
-	tag_ape_scan2(path_fs, &print_comment_handler, &client);
-	tag_id3_scan(path_fs, &print_comment_handler, &client);
+	tag_ape_scan2(path_fs, &print_comment_handler, &r);
+	tag_id3_scan(path_fs, &print_comment_handler, &r);
 
 	return CommandResult::OK;
 
@@ -219,6 +216,8 @@ translate_uri(const char *uri)
 CommandResult
 handle_read_comments(Client &client, Request args)
 {
+	Response r(client);
+
 	assert(args.size == 1);
 	const char *const uri = translate_uri(args.front());
 
@@ -227,25 +226,23 @@ handle_read_comments(Client &client, Request args)
 		const char *path_utf8 = uri + 7;
 		AllocatedPath path_fs = AllocatedPath::FromUTF8(path_utf8);
 		if (path_fs.IsNull()) {
-			command_error(client, ACK_ERROR_NO_EXIST,
-				      "unsupported file name");
+			r.Error(ACK_ERROR_NO_EXIST, "unsupported file name");
 			return CommandResult::ERROR;
 		}
 
 		Error error;
 		if (!client.AllowFile(path_fs, error))
-			return print_error(client, error);
+			return print_error(r, error);
 
-		return read_file_comments(client, path_fs);
+		return read_file_comments(r, path_fs);
 	} else if (uri_has_scheme(uri)) {
-		return read_stream_comments(client, uri);
+		return read_stream_comments(r, uri);
 	} else if (!PathTraitsUTF8::IsAbsolute(uri)) {
 #ifdef ENABLE_DATABASE
 		const Storage *storage = client.GetStorage();
 		if (storage == nullptr) {
 #endif
-			command_error(client, ACK_ERROR_NO_EXIST,
-				      "No database");
+			r.Error(ACK_ERROR_NO_EXIST, "No database");
 			return CommandResult::ERROR;
 #ifdef ENABLE_DATABASE
 		}
@@ -253,21 +250,20 @@ handle_read_comments(Client &client, Request args)
 		{
 			AllocatedPath path_fs = storage->MapFS(uri);
 			if (!path_fs.IsNull())
-				return read_file_comments(client, path_fs);
+				return read_file_comments(r, path_fs);
 		}
 
 		{
 			const std::string uri2 = storage->MapUTF8(uri);
 			if (uri_has_scheme(uri2.c_str()))
-				return read_stream_comments(client,
-							    uri2.c_str());
+				return read_stream_comments(r, uri2.c_str());
 		}
 
-		command_error(client, ACK_ERROR_NO_EXIST, "No such file");
+		r.Error(ACK_ERROR_NO_EXIST, "No such file");
 		return CommandResult::ERROR;
 #endif
 	} else {
-		command_error(client, ACK_ERROR_NO_EXIST, "No such file");
+		r.Error(ACK_ERROR_NO_EXIST, "No such file");
 		return CommandResult::ERROR;
 	}
 }
diff --git a/src/command/MessageCommands.cxx b/src/command/MessageCommands.cxx
index 24c9ddf2d..62d47ff0e 100644
--- a/src/command/MessageCommands.cxx
+++ b/src/command/MessageCommands.cxx
@@ -22,9 +22,9 @@
 #include "Request.hxx"
 #include "client/Client.hxx"
 #include "client/ClientList.hxx"
+#include "client/Response.hxx"
 #include "Instance.hxx"
 #include "Partition.hxx"
-#include "protocol/Result.hxx"
 #include "util/ConstBuffer.hxx"
 
 #include <set>
@@ -35,6 +35,8 @@
 CommandResult
 handle_subscribe(Client &client, Request args)
 {
+	Response r(client);
+
 	assert(args.size == 1);
 	const char *const channel_name = args[0];
 
@@ -43,18 +45,15 @@ handle_subscribe(Client &client, Request args)
 		return CommandResult::OK;
 
 	case Client::SubscribeResult::INVALID:
-		command_error(client, ACK_ERROR_ARG,
-			      "invalid channel name");
+		r.Error(ACK_ERROR_ARG, "invalid channel name");
 		return CommandResult::ERROR;
 
 	case Client::SubscribeResult::ALREADY:
-		command_error(client, ACK_ERROR_EXIST,
-			      "already subscribed to this channel");
+		r.Error(ACK_ERROR_EXIST, "already subscribed to this channel");
 		return CommandResult::ERROR;
 
 	case Client::SubscribeResult::FULL:
-		command_error(client, ACK_ERROR_EXIST,
-			      "subscription list is full");
+		r.Error(ACK_ERROR_EXIST, "subscription list is full");
 		return CommandResult::ERROR;
 	}
 
@@ -66,14 +65,15 @@ handle_subscribe(Client &client, Request args)
 CommandResult
 handle_unsubscribe(Client &client, Request args)
 {
+	Response r(client);
+
 	assert(args.size == 1);
 	const char *const channel_name = args[0];
 
 	if (client.Unsubscribe(channel_name))
 		return CommandResult::OK;
 	else {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "not subscribed to this channel");
+		r.Error(ACK_ERROR_NO_EXIST, "not subscribed to this channel");
 		return CommandResult::ERROR;
 	}
 }
@@ -88,8 +88,9 @@ handle_channels(Client &client, gcc_unused Request args)
 		channels.insert(c.subscriptions.begin(),
 				c.subscriptions.end());
 
+	Response r(client);
 	for (const auto &channel : channels)
-		client_printf(client, "channel: %s\n", channel.c_str());
+		r.Format("channel: %s\n", channel.c_str());
 
 	return CommandResult::OK;
 }
@@ -100,11 +101,12 @@ handle_read_messages(Client &client,
 {
 	assert(args.IsEmpty());
 
+	Response r(client);
 	while (!client.messages.empty()) {
 		const ClientMessage &msg = client.messages.front();
 
-		client_printf(client, "channel: %s\nmessage: %s\n",
-			      msg.GetChannel(), msg.GetMessage());
+		r.Format("channel: %s\nmessage: %s\n",
+			 msg.GetChannel(), msg.GetMessage());
 		client.messages.pop_front();
 	}
 
@@ -119,9 +121,9 @@ handle_send_message(Client &client, Request args)
 	const char *const channel_name = args[0];
 	const char *const message_text = args[1];
 
+	Response r(client);
 	if (!client_message_valid_channel_name(channel_name)) {
-		command_error(client, ACK_ERROR_ARG,
-			      "invalid channel name");
+		r.Error(ACK_ERROR_ARG, "invalid channel name");
 		return CommandResult::ERROR;
 	}
 
@@ -134,8 +136,8 @@ handle_send_message(Client &client, Request args)
 	if (sent)
 		return CommandResult::OK;
 	else {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "nobody is subscribed to this channel");
+		r.Error(ACK_ERROR_NO_EXIST,
+			"nobody is subscribed to this channel");
 		return CommandResult::ERROR;
 	}
 }
diff --git a/src/command/NeighborCommands.cxx b/src/command/NeighborCommands.cxx
index 10dbe3074..d1b2ec7c7 100644
--- a/src/command/NeighborCommands.cxx
+++ b/src/command/NeighborCommands.cxx
@@ -21,9 +21,9 @@
 #include "NeighborCommands.hxx"
 #include "Request.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "Instance.hxx"
 #include "Partition.hxx"
-#include "protocol/Result.hxx"
 #include "neighbor/Glue.hxx"
 #include "neighbor/Info.hxx"
 #include "util/ConstBuffer.hxx"
@@ -42,19 +42,19 @@ neighbor_commands_available(const Instance &instance)
 CommandResult
 handle_listneighbors(Client &client, gcc_unused Request args)
 {
+	Response r(client);
+
 	const NeighborGlue *const neighbors =
 		client.partition.instance.neighbors;
 	if (neighbors == nullptr) {
-		command_error(client, ACK_ERROR_UNKNOWN,
-			      "No neighbor plugin configured");
+		r.Error(ACK_ERROR_UNKNOWN, "No neighbor plugin configured");
 		return CommandResult::ERROR;
 	}
 
 	for (const auto &i : neighbors->GetList())
-		client_printf(client,
-			      "neighbor: %s\n"
-			      "name: %s\n",
-			      i.uri.c_str(),
-			      i.display_name.c_str());
+		r.Format("neighbor: %s\n"
+			 "name: %s\n",
+			 i.uri.c_str(),
+			 i.display_name.c_str());
 	return CommandResult::OK;
 }
diff --git a/src/command/OtherCommands.cxx b/src/command/OtherCommands.cxx
index 2e20d8dd5..c1346ca44 100644
--- a/src/command/OtherCommands.cxx
+++ b/src/command/OtherCommands.cxx
@@ -32,7 +32,6 @@
 #include "tag/TagHandler.hxx"
 #include "TimePrint.hxx"
 #include "decoder/DecoderPrint.hxx"
-#include "protocol/Result.hxx"
 #include "ls.hxx"
 #include "mixer/Volume.hxx"
 #include "util/UriUtil.hxx"
@@ -44,6 +43,7 @@
 #include "PlaylistFile.hxx"
 #include "db/PlaylistVector.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "Partition.hxx"
 #include "Instance.hxx"
 #include "Idle.hxx"
@@ -58,36 +58,39 @@
 #include <string.h>
 
 static void
-print_spl_list(Client &client, const PlaylistVector &list)
+print_spl_list(Response &r, const PlaylistVector &list)
 {
 	for (const auto &i : list) {
-		client_printf(client, "playlist: %s\n", i.name.c_str());
+		r.Format("playlist: %s\n", i.name.c_str());
 
 		if (i.mtime > 0)
-			time_print(client, "Last-Modified", i.mtime);
+			time_print(r, "Last-Modified", i.mtime);
 	}
 }
 
 CommandResult
 handle_urlhandlers(Client &client, gcc_unused Request args)
 {
+	Response r(client);
 	if (client.IsLocal())
-		client_puts(client, "handler: file://\n");
-	print_supported_uri_schemes(client);
+		r.Format("handler: file://\n");
+	print_supported_uri_schemes(r);
 	return CommandResult::OK;
 }
 
 CommandResult
 handle_decoders(Client &client, gcc_unused Request args)
 {
-	decoder_list_print(client);
+	Response r(client);
+	decoder_list_print(r);
 	return CommandResult::OK;
 }
 
 CommandResult
 handle_tagtypes(Client &client, gcc_unused Request args)
 {
-	tag_print_types(client);
+	Response r(client);
+	tag_print_types(r);
 	return CommandResult::OK;
 }
 
@@ -106,14 +109,16 @@ handle_close(gcc_unused Client &client, gcc_unused Request args)
 static void
 print_tag(TagType type, const char *value, void *ctx)
 {
-	Client &client = *(Client *)ctx;
+	auto &r = *(Response *)ctx;
 
-	tag_print(client, type, value);
+	tag_print(r, type, value);
 }
 
 CommandResult
 handle_listfiles(Client &client, Request args)
 {
+	Response r(client);
+
 	/* default is root directory */
 	const auto uri = args.GetOptional(0, "");
 
@@ -124,7 +129,7 @@ handle_listfiles(Client &client, Request args)
 #ifdef ENABLE_DATABASE
 	if (uri_has_scheme(uri))
 		/* use storage plugin to list remote directory */
-		return handle_listfiles_storage(client, uri);
+		return handle_listfiles_storage(r, uri);
 
 	/* must be a path relative to the configured
 	   music_directory */
@@ -132,14 +137,14 @@ handle_listfiles(Client &client, Request args)
 	if (client.partition.instance.storage != nullptr)
 		/* if we have a storage instance, obtain a list of
 		   files from it */
-		return handle_listfiles_storage(client,
+		return handle_listfiles_storage(r,
 						*client.partition.instance.storage,
 						uri);
 
 	/* fall back to entries from database if we have no storage */
 	return handle_listfiles_db(client, uri);
 #else
-	command_error(client, ACK_ERROR_NO_EXIST, "No database");
+	r.Error(ACK_ERROR_NO_EXIST, "No database");
 	return CommandResult::ERROR;
 #endif
 }
@@ -156,42 +161,40 @@ handle_lsinfo(Client &client, Request args)
 	/* default is root directory */
 	const auto uri = args.GetOptional(0, "");
 
+	Response r(client);
+
 	if (memcmp(uri, "file:///", 8) == 0) {
 		/* print information about an arbitrary local file */
 		const char *path_utf8 = uri + 7;
 		const auto path_fs = AllocatedPath::FromUTF8(path_utf8);
 
 		if (path_fs.IsNull()) {
-			command_error(client, ACK_ERROR_NO_EXIST,
-				      "unsupported file name");
+			r.Error(ACK_ERROR_NO_EXIST, "unsupported file name");
 			return CommandResult::ERROR;
 		}
 
 		Error error;
 		if (!client.AllowFile(path_fs, error))
-			return print_error(client, error);
+			return print_error(r, error);
 
 		DetachedSong song(path_utf8);
 		if (!song.Update()) {
-			command_error(client, ACK_ERROR_NO_EXIST,
-				      "No such file");
+			r.Error(ACK_ERROR_NO_EXIST, "No such file");
 			return CommandResult::ERROR;
 		}
 
-		song_print_info(client, song);
+		song_print_info(r, client.partition, song);
 		return CommandResult::OK;
 	}
 
 	if (uri_has_scheme(uri)) {
 		if (!uri_supported_scheme(uri)) {
-			command_error(client, ACK_ERROR_NO_EXIST,
-				      "unsupported URI scheme");
+			r.Error(ACK_ERROR_NO_EXIST, "unsupported URI scheme");
 			return CommandResult::ERROR;
 		}
 
-		if (!tag_stream_scan(uri, print_tag_handler, &client)) {
-			command_error(client, ACK_ERROR_NO_EXIST,
-				      "No such file");
+		if (!tag_stream_scan(uri, print_tag_handler, &r)) {
+			r.Error(ACK_ERROR_NO_EXIST, "No such file");
 			return CommandResult::ERROR;
 		}
 
@@ -207,10 +210,10 @@ handle_lsinfo(Client &client, Request args)
 	if (isRootDirectory(uri)) {
 		Error error;
 		const auto &list = ListPlaylistFiles(error);
-		print_spl_list(client, list);
+		print_spl_list(r, list);
 	} else {
 #ifndef ENABLE_DATABASE
-		command_error(client, ACK_ERROR_NO_EXIST, "No database");
+		r.Error(ACK_ERROR_NO_EXIST, "No database");
 		return CommandResult::ERROR;
 #endif
 	}
@@ -224,13 +227,14 @@ static CommandResult
 handle_update(Client &client, UpdateService &update,
 	      const char *uri_utf8, bool discard)
 {
+	Response r(client);
+
 	unsigned ret = update.Enqueue(uri_utf8, discard);
 	if (ret > 0) {
-		client_printf(client, "updating_db: %i\n", ret);
+		r.Format("updating_db: %i\n", ret);
 		return CommandResult::OK;
 	} else {
-		command_error(client, ACK_ERROR_UPDATE_ALREADY,
-			      "already updating");
+		r.Error(ACK_ERROR_UPDATE_ALREADY, "already updating");
 		return CommandResult::ERROR;
 	}
 }
@@ -239,17 +243,19 @@ static CommandResult
 handle_update(Client &client, Database &db,
 	      const char *uri_utf8, bool discard)
 {
+	Response r(client);
+
 	Error error;
 	unsigned id = db.Update(uri_utf8, discard, error);
 	if (id > 0) {
-		client_printf(client, "updating_db: %i\n", id);
+		r.Format("updating_db: %i\n", id);
 		return CommandResult::OK;
 	} else if (error.IsDefined()) {
-		return print_error(client, error);
+		return print_error(r, error);
 	} else {
 		/* Database::Update() has returned 0 without setting
 		   the Error: the method is not implemented */
-		command_error(client, ACK_ERROR_NO_EXIST, "Not implemented");
+		r.Error(ACK_ERROR_NO_EXIST, "Not implemented");
 		return CommandResult::ERROR;
 	}
 }
@@ -259,6 +265,8 @@ handle_update(Client &client, Database &db,
 static CommandResult
 handle_update(Client &client, Request args, bool discard)
 {
+	Response r(client);
+
 #ifdef ENABLE_DATABASE
 	const char *path = "";
 
@@ -270,8 +278,7 @@ handle_update(Client &client, Request args, bool discard)
 			/* backwards compatibility with MPD 0.15 */
 			path = "";
 		else if (!uri_safe_local(path)) {
-			command_error(client, ACK_ERROR_ARG,
-				      "Malformed path");
+			r.Error(ACK_ERROR_ARG, "Malformed path");
 			return CommandResult::ERROR;
 		}
 	}
@@ -288,7 +295,7 @@ handle_update(Client &client, Request args, bool discard)
 	(void)discard;
 #endif
 
-	command_error(client, ACK_ERROR_NO_EXIST, "No database");
+	r.Error(ACK_ERROR_NO_EXIST, "No database");
 	return CommandResult::ERROR;
 }
 
@@ -307,13 +314,14 @@ handle_rescan(Client &client, gcc_unused Request args)
 CommandResult
 handle_setvol(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned level;
-	if (!args.Parse(0, level, client, 100))
+	if (!args.Parse(0, level, r, 100))
 		return CommandResult::ERROR;
 
 	if (!volume_level_change(client.partition.outputs, level)) {
-		command_error(client, ACK_ERROR_SYSTEM,
-			      "problems setting volume");
+		r.Error(ACK_ERROR_SYSTEM, "problems setting volume");
 		return CommandResult::ERROR;
 	}
 
@@ -323,13 +331,15 @@ handle_setvol(Client &client, Request args)
 CommandResult
 handle_volume(Client &client, Request args)
 {
+	Response r(client);
+
 	int relative;
-	if (!args.Parse(0, relative, client,  -100, 100))
+	if (!args.Parse(0, relative, r,  -100, 100))
 		return CommandResult::ERROR;
 
 	const int old_volume = volume_level_get(client.partition.outputs);
 	if (old_volume < 0) {
-		command_error(client, ACK_ERROR_SYSTEM, "No mixer");
+		r.Error(ACK_ERROR_SYSTEM, "No mixer");
 		return CommandResult::ERROR;
 	}
 
@@ -341,8 +351,7 @@ handle_volume(Client &client, Request args)
 
 	if (new_volume != old_volume &&
 	    !volume_level_change(client.partition.outputs, new_volume)) {
-		command_error(client, ACK_ERROR_SYSTEM,
-			      "problems setting volume");
+		r.Error(ACK_ERROR_SYSTEM, "problems setting volume");
 		return CommandResult::ERROR;
 	}
 
@@ -352,7 +361,8 @@ handle_volume(Client &client, Request args)
 CommandResult
 handle_stats(Client &client, gcc_unused Request args)
 {
-	stats_print(client);
+	Response r(client);
+	stats_print(r, client.partition);
 	return CommandResult::OK;
 }
 
@@ -365,10 +375,11 @@ handle_ping(gcc_unused Client &client, gcc_unused Request args)
 CommandResult
 handle_password(Client &client, Request args)
 {
-	unsigned permission = 0;
+	Response r(client);
 
+	unsigned permission = 0;
 	if (getPermissionFromPassword(args.front(), &permission) < 0) {
-		command_error(client, ACK_ERROR_PASSWORD, "incorrect password");
+		r.Error(ACK_ERROR_PASSWORD, "incorrect password");
 		return CommandResult::ERROR;
 	}
 
@@ -380,9 +391,11 @@ handle_password(Client &client, Request args)
 CommandResult
 handle_config(Client &client, gcc_unused Request args)
 {
+	Response r(client);
+
 	if (!client.IsLocal()) {
-		command_error(client, ACK_ERROR_PERMISSION,
-			      "Command only permitted to local clients");
+		r.Error(ACK_ERROR_PERMISSION,
+			"Command only permitted to local clients");
 		return CommandResult::ERROR;
 	}
 
@@ -390,7 +403,7 @@ handle_config(Client &client, gcc_unused Request args)
 	const Storage *storage = client.GetStorage();
 	if (storage != nullptr) {
 		const auto path = storage->MapUTF8("");
-		client_printf(client, "music_directory: %s\n", path.c_str());
+		r.Format("music_directory: %s\n", path.c_str());
 	}
 #endif
 
@@ -400,14 +413,14 @@ handle_config(Client &client, gcc_unused Request args)
 CommandResult
 handle_idle(Client &client, Request args)
 {
-	unsigned flags = 0;
+	Response r(client);
 
+	unsigned flags = 0;
 	for (const char *i : args) {
 		unsigned event = idle_parse_name(i);
 		if (event == 0) {
-			command_error(client, ACK_ERROR_ARG,
-				      "Unrecognized idle event: %s",
-				      i);
+			r.FormatError(ACK_ERROR_ARG,
+				      "Unrecognized idle event: %s", i);
 			return CommandResult::ERROR;
 		}
 
diff --git a/src/command/OutputCommands.cxx b/src/command/OutputCommands.cxx
index 39602aba8..472516860 100644
--- a/src/command/OutputCommands.cxx
+++ b/src/command/OutputCommands.cxx
@@ -22,23 +22,23 @@
 #include "Request.hxx"
 #include "output/OutputPrint.hxx"
 #include "output/OutputCommand.hxx"
-#include "protocol/Result.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "Partition.hxx"
 #include "util/ConstBuffer.hxx"
 
 CommandResult
 handle_enableoutput(Client &client, Request args)
 {
-	assert(args.size == 1);
+	Response r(client);
 
+	assert(args.size == 1);
 	unsigned device;
-	if (!args.Parse(0, device, client))
+	if (!args.Parse(0, device, r))
 		return CommandResult::ERROR;
 
 	if (!audio_output_enable_index(client.partition.outputs, device)) {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "No such audio output");
+		r.Error(ACK_ERROR_NO_EXIST, "No such audio output");
 		return CommandResult::ERROR;
 	}
 
@@ -48,15 +48,15 @@ handle_enableoutput(Client &client, Request args)
 CommandResult
 handle_disableoutput(Client &client, Request args)
 {
-	assert(args.size == 1);
+	Response r(client);
 
+	assert(args.size == 1);
 	unsigned device;
-	if (!args.Parse(0, device, client))
+	if (!args.Parse(0, device, r))
 		return CommandResult::ERROR;
 
 	if (!audio_output_disable_index(client.partition.outputs, device)) {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "No such audio output");
+		r.Error(ACK_ERROR_NO_EXIST, "No such audio output");
 		return CommandResult::ERROR;
 	}
 
@@ -66,15 +66,15 @@ handle_disableoutput(Client &client, Request args)
 CommandResult
 handle_toggleoutput(Client &client, Request args)
 {
-	assert(args.size == 1);
+	Response r(client);
 
+	assert(args.size == 1);
 	unsigned device;
-	if (!args.Parse(0, device, client))
+	if (!args.Parse(0, device, r))
 		return CommandResult::ERROR;
 
 	if (!audio_output_toggle_index(client.partition.outputs, device)) {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "No such audio output");
+		r.Error(ACK_ERROR_NO_EXIST, "No such audio output");
 		return CommandResult::ERROR;
 	}
 
@@ -86,7 +86,8 @@ handle_devices(Client &client, gcc_unused Request args)
 {
 	assert(args.IsEmpty());
 
-	printAudioDevices(client, client.partition.outputs);
+	Response r(client);
+	printAudioDevices(r, client.partition.outputs);
 
 	return CommandResult::OK;
 }
diff --git a/src/command/PlayerCommands.cxx b/src/command/PlayerCommands.cxx
index e0c0f61a8..3e58cecb1 100644
--- a/src/command/PlayerCommands.cxx
+++ b/src/command/PlayerCommands.cxx
@@ -24,10 +24,10 @@
 #include "queue/Playlist.hxx"
 #include "PlaylistPrint.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "mixer/Volume.hxx"
 #include "Partition.hxx"
 #include "Instance.hxx"
-#include "protocol/Result.hxx"
 #include "AudioFormat.hxx"
 #include "ReplayGainConfig.hxx"
 #include "util/ConstBuffer.hxx"
@@ -59,23 +59,27 @@
 CommandResult
 handle_play(Client &client, Request args)
 {
+	Response r(client);
+
 	int song = -1;
-	if (!args.ParseOptional(0, song, client))
+	if (!args.ParseOptional(0, song, r))
 		return CommandResult::ERROR;
 
 	PlaylistResult result = client.partition.PlayPosition(song);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
 handle_playid(Client &client, Request args)
 {
+	Response r(client);
+
 	int id = -1;
-	if (!args.ParseOptional(0, id, client))
+	if (!args.ParseOptional(0, id, r))
 		return CommandResult::ERROR;
 
 	PlaylistResult result = client.partition.PlayId(id);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
@@ -88,16 +92,19 @@ handle_stop(Client &client, gcc_unused Request args)
 CommandResult
 handle_currentsong(Client &client, gcc_unused Request args)
 {
-	playlist_print_current(client, client.playlist);
+	Response r(client);
+	playlist_print_current(r, client.partition, client.playlist);
 	return CommandResult::OK;
 }
 
 CommandResult
 handle_pause(Client &client, Request args)
 {
+	Response r(client);
+
 	if (!args.IsEmpty()) {
 		bool pause_flag;
-		if (!args.Parse(0, pause_flag, client))
+		if (!args.Parse(0, pause_flag, r))
 			return CommandResult::ERROR;
 
 		client.player_control.SetPause(pause_flag);
@@ -127,68 +134,64 @@ handle_status(Client &client, gcc_unused Request args)
 		break;
 	}
 
+	Response r(client);
+
 	const playlist &playlist = client.playlist;
-	client_printf(client,
-		      "volume: %i\n"
-		      COMMAND_STATUS_REPEAT ": %i\n"
-		      COMMAND_STATUS_RANDOM ": %i\n"
-		      COMMAND_STATUS_SINGLE ": %i\n"
-		      COMMAND_STATUS_CONSUME ": %i\n"
-		      COMMAND_STATUS_PLAYLIST ": %li\n"
-		      COMMAND_STATUS_PLAYLIST_LENGTH ": %i\n"
-		      COMMAND_STATUS_MIXRAMPDB ": %f\n"
-		      COMMAND_STATUS_STATE ": %s\n",
-		      volume_level_get(client.partition.outputs),
-		      playlist.GetRepeat(),
-		      playlist.GetRandom(),
-		      playlist.GetSingle(),
-		      playlist.GetConsume(),
-		      (unsigned long)playlist.GetVersion(),
-		      playlist.GetLength(),
-		      client.player_control.GetMixRampDb(),
-		      state);
+	r.Format("volume: %i\n"
+		 COMMAND_STATUS_REPEAT ": %i\n"
+		 COMMAND_STATUS_RANDOM ": %i\n"
+		 COMMAND_STATUS_SINGLE ": %i\n"
+		 COMMAND_STATUS_CONSUME ": %i\n"
+		 COMMAND_STATUS_PLAYLIST ": %li\n"
+		 COMMAND_STATUS_PLAYLIST_LENGTH ": %i\n"
+		 COMMAND_STATUS_MIXRAMPDB ": %f\n"
+		 COMMAND_STATUS_STATE ": %s\n",
+		 volume_level_get(client.partition.outputs),
+		 playlist.GetRepeat(),
+		 playlist.GetRandom(),
+		 playlist.GetSingle(),
+		 playlist.GetConsume(),
+		 (unsigned long)playlist.GetVersion(),
+		 playlist.GetLength(),
+		 client.player_control.GetMixRampDb(),
+		 state);
 
 	if (client.player_control.GetCrossFade() > 0)
-		client_printf(client,
-			      COMMAND_STATUS_CROSSFADE ": %i\n",
-			      int(client.player_control.GetCrossFade() + 0.5));
+		r.Format(COMMAND_STATUS_CROSSFADE ": %i\n",
+			 int(client.player_control.GetCrossFade() + 0.5));
 
 	if (client.player_control.GetMixRampDelay() > 0)
-		client_printf(client,
-			      COMMAND_STATUS_MIXRAMPDELAY ": %f\n",
-			      client.player_control.GetMixRampDelay());
+		r.Format(COMMAND_STATUS_MIXRAMPDELAY ": %f\n",
+			 client.player_control.GetMixRampDelay());
 
 	song = playlist.GetCurrentPosition();
 	if (song >= 0) {
-		client_printf(client,
-			      COMMAND_STATUS_SONG ": %i\n"
-			      COMMAND_STATUS_SONGID ": %u\n",
-			      song, playlist.PositionToId(song));
+		r.Format(COMMAND_STATUS_SONG ": %i\n"
+			 COMMAND_STATUS_SONGID ": %u\n",
+			 song, playlist.PositionToId(song));
 	}
 
 	if (player_status.state != PlayerState::STOP) {
-		client_printf(client,
-			      COMMAND_STATUS_TIME ": %i:%i\n"
-			      "elapsed: %1.3f\n"
-			      COMMAND_STATUS_BITRATE ": %u\n",
-			      player_status.elapsed_time.RoundS(),
-			      player_status.total_time.IsNegative()
-			      ? 0u
-			      : unsigned(player_status.total_time.RoundS()),
-			      player_status.elapsed_time.ToDoubleS(),
-			      player_status.bit_rate);
+		r.Format(COMMAND_STATUS_TIME ": %i:%i\n"
+			 "elapsed: %1.3f\n"
+			 COMMAND_STATUS_BITRATE ": %u\n",
+			 player_status.elapsed_time.RoundS(),
+			 player_status.total_time.IsNegative()
+			 ? 0u
+			 : unsigned(player_status.total_time.RoundS()),
+			 player_status.elapsed_time.ToDoubleS(),
+			 player_status.bit_rate);
 
 		if (!player_status.total_time.IsNegative())
-			client_printf(client, "duration: %1.3f\n",
-				      player_status.total_time.ToDoubleS());
+			r.Format("duration: %1.3f\n",
+				 player_status.total_time.ToDoubleS());
 
 		if (player_status.audio_format.IsDefined()) {
 			struct audio_format_string af_string;
 
-			client_printf(client,
-				      COMMAND_STATUS_AUDIO ": %s\n",
-				      audio_format_to_string(player_status.audio_format,
-							     &af_string));
+			r.Format(COMMAND_STATUS_AUDIO ": %s\n",
+				 audio_format_to_string(player_status.audio_format,
+							&af_string));
 		}
 	}
 
@@ -198,25 +201,21 @@ handle_status(Client &client, gcc_unused Request args)
 		? update_service->GetId()
 		: 0;
 	if (updateJobId != 0) {
-		client_printf(client,
-			      COMMAND_STATUS_UPDATING_DB ": %i\n",
-			      updateJobId);
+		r.Format(COMMAND_STATUS_UPDATING_DB ": %i\n",
+			 updateJobId);
 	}
 #endif
 
 	Error error = client.player_control.LockGetError();
 	if (error.IsDefined())
-		client_printf(client,
-			      COMMAND_STATUS_ERROR ": %s\n",
-			      error.GetMessage());
+		r.Format(COMMAND_STATUS_ERROR ": %s\n",
+			 error.GetMessage());
 
 	song = playlist.GetNextPosition();
-	if (song >= 0) {
-		client_printf(client,
-			      COMMAND_STATUS_NEXTSONG ": %i\n"
-			      COMMAND_STATUS_NEXTSONGID ": %u\n",
-			      song, playlist.PositionToId(song));
-	}
+	if (song >= 0)
+		r.Format(COMMAND_STATUS_NEXTSONG ": %i\n"
+			 COMMAND_STATUS_NEXTSONGID ": %u\n",
+			 song, playlist.PositionToId(song));
 
 	return CommandResult::OK;
 }
@@ -247,8 +246,10 @@ handle_previous(Client &client, gcc_unused Request args)
 CommandResult
 handle_repeat(Client &client, Request args)
 {
+	Response r(client);
+
 	bool status;
-	if (!args.Parse(0, status, client))
+	if (!args.Parse(0, status, r))
 		return CommandResult::ERROR;
 
 	client.partition.SetRepeat(status);
@@ -258,8 +259,10 @@ handle_repeat(Client &client, Request args)
 CommandResult
 handle_single(Client &client, Request args)
 {
+	Response r(client);
+
 	bool status;
-	if (!args.Parse(0, status, client))
+	if (!args.Parse(0, status, r))
 		return CommandResult::ERROR;
 
 	client.partition.SetSingle(status);
@@ -269,8 +272,10 @@ handle_single(Client &client, Request args)
 CommandResult
 handle_consume(Client &client, Request args)
 {
+	Response r(client);
+
 	bool status;
-	if (!args.Parse(0, status, client))
+	if (!args.Parse(0, status, r))
 		return CommandResult::ERROR;
 
 	client.partition.SetConsume(status);
@@ -280,8 +285,10 @@ handle_consume(Client &client, Request args)
 CommandResult
 handle_random(Client &client, Request args)
 {
+	Response r(client);
+
 	bool status;
-	if (!args.Parse(0, status, client))
+	if (!args.Parse(0, status, r))
 		return CommandResult::ERROR;
 
 	client.partition.SetRandom(status);
@@ -299,53 +306,58 @@ handle_clearerror(gcc_unused Client &client, gcc_unused Request args)
 CommandResult
 handle_seek(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned song;
 	SongTime seek_time;
-
-	if (!args.Parse(0, song, client))
-		return CommandResult::ERROR;
-	if (!args.Parse(1, seek_time, client))
+	if (!args.Parse(0, song, r) || !args.Parse(1, seek_time, r))
 		return CommandResult::ERROR;
 
 	PlaylistResult result =
 		client.partition.SeekSongPosition(song, seek_time);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
 handle_seekid(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned id;
 	SongTime seek_time;
-	if (!args.Parse(0, id, client))
+	if (!args.Parse(0, id, r))
 		return CommandResult::ERROR;
-	if (!args.Parse(1, seek_time, client))
+	if (!args.Parse(1, seek_time, r))
 		return CommandResult::ERROR;
 
 	PlaylistResult result =
 		client.partition.SeekSongId(id, seek_time);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
 handle_seekcur(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *p = args.front();
 	bool relative = *p == '+' || *p == '-';
 	SignedSongTime seek_time;
-	if (!ParseCommandArg(client, seek_time, p))
+	if (!ParseCommandArg(r, seek_time, p))
 		return CommandResult::ERROR;
 
 	PlaylistResult result =
 		client.partition.SeekCurrent(seek_time, relative);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
 handle_crossfade(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned xfade_time;
-	if (!args.Parse(0, xfade_time, client))
+	if (!args.Parse(0, xfade_time, r))
 		return CommandResult::ERROR;
 
 	client.player_control.SetCrossFade(xfade_time);
@@ -355,8 +367,10 @@ handle_crossfade(Client &client, Request args)
 CommandResult
 handle_mixrampdb(Client &client, Request args)
 {
+	Response r(client);
+
 	float db;
-	if (!args.Parse(0, db, client))
+	if (!args.Parse(0, db, r))
 		return CommandResult::ERROR;
 
 	client.player_control.SetMixRampDb(db);
@@ -366,8 +380,10 @@ handle_mixrampdb(Client &client, Request args)
 CommandResult
 handle_mixrampdelay(Client &client, Request args)
 {
+	Response r(client);
+
 	float delay_secs;
-	if (!args.Parse(0, delay_secs, client))
+	if (!args.Parse(0, delay_secs, r))
 		return CommandResult::ERROR;
 
 	client.player_control.SetMixRampDelay(delay_secs);
@@ -378,9 +394,10 @@ handle_mixrampdelay(Client &client, Request args)
 CommandResult
 handle_replay_gain_mode(Client &client, Request args)
 {
+	Response r(client);
+
 	if (!replay_gain_set_mode_string(args.front())) {
-		command_error(client, ACK_ERROR_ARG,
-			      "Unrecognized replay gain mode");
+		r.Error(ACK_ERROR_ARG, "Unrecognized replay gain mode");
 		return CommandResult::ERROR;
 	}
 
@@ -391,7 +408,7 @@ handle_replay_gain_mode(Client &client, Request args)
 CommandResult
 handle_replay_gain_status(Client &client, gcc_unused Request args)
 {
-	client_printf(client, "replay_gain_mode: %s\n",
-		      replay_gain_get_mode_string());
+	Response r(client);
+	r.Format("replay_gain_mode: %s\n", replay_gain_get_mode_string());
 	return CommandResult::OK;
 }
diff --git a/src/command/PlaylistCommands.cxx b/src/command/PlaylistCommands.cxx
index 0122cecbd..1e075e088 100644
--- a/src/command/PlaylistCommands.cxx
+++ b/src/command/PlaylistCommands.cxx
@@ -33,7 +33,7 @@
 #include "queue/Playlist.hxx"
 #include "TimePrint.hxx"
 #include "client/Client.hxx"
-#include "protocol/Result.hxx"
+#include "client/Response.hxx"
 #include "ls.hxx"
 #include "Mapper.hxx"
 #include "fs/AllocatedPath.hxx"
@@ -48,30 +48,33 @@ playlist_commands_available()
 }
 
 static void
-print_spl_list(Client &client, const PlaylistVector &list)
+print_spl_list(Response &r, const PlaylistVector &list)
 {
 	for (const auto &i : list) {
-		client_printf(client, "playlist: %s\n", i.name.c_str());
+		r.Format("playlist: %s\n", i.name.c_str());
 
 		if (i.mtime > 0)
-			time_print(client, "Last-Modified", i.mtime);
+			time_print(r, "Last-Modified", i.mtime);
 	}
 }
 
 CommandResult
 handle_save(Client &client, Request args)
 {
+	Response r(client);
 	Error error;
 	return spl_save_playlist(args.front(), client.playlist, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_load(Client &client, Request args)
 {
+	Response r(client);
+
 	RangeArg range = RangeArg::All();
-	if (!args.ParseOptional(1, range, client))
+	if (!args.ParseOptional(1, range, r))
 		return CommandResult::ERROR;
 
 	const ScopeBulkEdit bulk_edit(client.partition);
@@ -82,7 +85,7 @@ handle_load(Client &client, Request args)
 				      range.start, range.end,
 				      client.playlist,
 				      client.player_control, loader, error))
-		return print_error(client, error);
+		return print_error(r, error);
 
 	return CommandResult::OK;
 }
@@ -90,97 +93,114 @@ handle_load(Client &client, Request args)
 CommandResult
 handle_listplaylist(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *const name = args.front();
 
-	if (playlist_file_print(client, name, false))
+	if (playlist_file_print(r, client.partition, SongLoader(client),
+				name, false))
 		return CommandResult::OK;
 
 	Error error;
-	return spl_print(client, name, false, error)
+	return spl_print(r, client.partition, name, false, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_listplaylistinfo(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *const name = args.front();
 
-	if (playlist_file_print(client, name, true))
+	if (playlist_file_print(r, client.partition, SongLoader(client),
+				name, true))
 		return CommandResult::OK;
 
 	Error error;
-	return spl_print(client, name, true, error)
+	return spl_print(r, client.partition, name, true, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_rm(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *const name = args.front();
 
 	Error error;
 	return spl_delete(name, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_rename(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *const old_name = args[0];
 	const char *const new_name = args[1];
 
 	Error error;
 	return spl_rename(old_name, new_name, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_playlistdelete(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *const name = args[0];
 	unsigned from;
-	if (!args.Parse(1, from, client))
+	if (!args.Parse(1, from, r))
 		return CommandResult::ERROR;
 
 	Error error;
 	return spl_remove_index(name, from, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_playlistmove(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *const name = args.front();
 	unsigned from, to;
-	if (!args.Parse(1, from, client) ||
-	    !args.Parse(2, to, client))
+	if (!args.Parse(1, from, r) || !args.Parse(2, to, r))
 		return CommandResult::ERROR;
 
 	Error error;
 	return spl_move_index(name, from, to, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_playlistclear(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *const name = args.front();
 
 	Error error;
 	return spl_clear(name, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 }
 
 CommandResult
 handle_playlistadd(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *const playlist = args[0];
 	const char *const uri = args[1];
 
@@ -193,7 +213,7 @@ handle_playlistadd(Client &client, Request args)
 #ifdef ENABLE_DATABASE
 		const Database *db = client.GetDatabase(error);
 		if (db == nullptr)
-			return print_error(client, error);
+			return print_error(r, error);
 
 		success = search_add_to_playlist(*db, *client.GetStorage(),
 						 uri, playlist, nullptr,
@@ -204,22 +224,23 @@ handle_playlistadd(Client &client, Request args)
 	}
 
 	if (!success && !error.IsDefined()) {
-		command_error(client, ACK_ERROR_NO_EXIST,
-			      "directory or file not found");
+		r.Error(ACK_ERROR_NO_EXIST, "directory or file not found");
 		return CommandResult::ERROR;
 	}
 
-	return success ? CommandResult::OK : print_error(client, error);
+	return success ? CommandResult::OK : print_error(r, error);
 }
 
 CommandResult
 handle_listplaylists(Client &client, gcc_unused Request args)
 {
+	Response r(client);
+
 	Error error;
 	const auto list = ListPlaylistFiles(error);
 	if (list.empty() && error.IsDefined())
-		return print_error(client, error);
+		return print_error(r, error);
 
-	print_spl_list(client, list);
+	print_spl_list(r, list);
 	return CommandResult::OK;
 }
diff --git a/src/command/QueueCommands.cxx b/src/command/QueueCommands.cxx
index 1413a68c9..850631ded 100644
--- a/src/command/QueueCommands.cxx
+++ b/src/command/QueueCommands.cxx
@@ -28,9 +28,9 @@
 #include "queue/Playlist.hxx"
 #include "PlaylistPrint.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "Partition.hxx"
 #include "BulkEdit.hxx"
-#include "protocol/Result.hxx"
 #include "ls.hxx"
 #include "util/ConstBuffer.hxx"
 #include "util/UriUtil.hxx"
@@ -56,6 +56,8 @@ translate_uri(const char *uri)
 CommandResult
 handle_add(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *uri = args.front();
 	if (memcmp(uri, "/", 2) == 0)
 		/* this URI is malformed, but some clients are buggy
@@ -72,7 +74,7 @@ handle_add(Client &client, Request args)
 		Error error;
 		unsigned id = client.partition.AppendURI(loader, uri, error);
 		if (id == 0)
-			return print_error(client, error);
+			return print_error(r, error);
 
 		return CommandResult::OK;
 	}
@@ -84,9 +86,9 @@ handle_add(Client &client, Request args)
 	Error error;
 	return AddFromDatabase(client.partition, selection, error)
 		? CommandResult::OK
-		: print_error(client, error);
+		: print_error(r, error);
 #else
-	command_error(client, ACK_ERROR_NO_EXIST, "No database");
+	r.Error(ACK_ERROR_NO_EXIST, "No database");
 	return CommandResult::ERROR;
 #endif
 }
@@ -94,29 +96,31 @@ handle_add(Client &client, Request args)
 CommandResult
 handle_addid(Client &client, Request args)
 {
+	Response r(client);
+
 	const char *const uri = translate_uri(args.front());
 
 	const SongLoader loader(client);
 	Error error;
 	unsigned added_id = client.partition.AppendURI(loader, uri, error);
 	if (added_id == 0)
-		return print_error(client, error);
+		return print_error(r, error);
 
 	if (args.size == 2) {
 		unsigned to;
-		if (!args.Parse(1, to, client))
+		if (!args.Parse(1, to, r))
 			return CommandResult::ERROR;
 
 		PlaylistResult result = client.partition.MoveId(added_id, to);
 		if (result != PlaylistResult::SUCCESS) {
 			CommandResult ret =
-				print_playlist_result(client, result);
+				print_playlist_result(r, result);
 			client.partition.DeleteId(added_id);
 			return ret;
 		}
 	}
 
-	client_printf(client, "Id: %u\n", added_id);
+	r.Format("Id: %u\n", added_id);
 	return CommandResult::OK;
 }
 
@@ -154,13 +158,15 @@ parse_time_range(const char *p, SongTime &start_r, SongTime &end_r)
 CommandResult
 handle_rangeid(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned id;
-	if (!args.Parse(0, id, client))
+	if (!args.Parse(0, id, r))
 		return CommandResult::ERROR;
 
 	SongTime start, end;
 	if (!parse_time_range(args[1], start, end)) {
-		command_error(client, ACK_ERROR_ARG, "Bad range");
+		r.Error(ACK_ERROR_ARG, "Bad range");
 		return CommandResult::ERROR;
 	}
 
@@ -168,7 +174,7 @@ handle_rangeid(Client &client, Request args)
 	if (!client.partition.playlist.SetSongIdRange(client.partition.pc,
 						      id, start, end,
 						      error))
-		return print_error(client, error);
+		return print_error(r, error);
 
 	return CommandResult::OK;
 }
@@ -176,37 +182,44 @@ handle_rangeid(Client &client, Request args)
 CommandResult
 handle_delete(Client &client, Request args)
 {
+	Response r(client);
+
 	RangeArg range;
-	if (!args.Parse(0, range, client))
+	if (!args.Parse(0, range, r))
 		return CommandResult::ERROR;
 
 	auto result = client.partition.DeleteRange(range.start, range.end);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
 handle_deleteid(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned id;
-	if (!args.Parse(0, id, client))
+	if (!args.Parse(0, id, r))
 		return CommandResult::ERROR;
 
 	PlaylistResult result = client.partition.DeleteId(id);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
 handle_playlist(Client &client, gcc_unused Request args)
 {
-	playlist_print_uris(client, client.playlist);
+	Response r(client);
+	playlist_print_uris(r, client.partition, client.playlist);
 	return CommandResult::OK;
 }
 
 CommandResult
 handle_shuffle(gcc_unused Client &client, Request args)
 {
+	Response r(client);
+
 	RangeArg range = RangeArg::All();
-	if (!args.ParseOptional(0, range, client))
+	if (!args.ParseOptional(0, range, r))
 		return CommandResult::ERROR;
 
 	client.partition.Shuffle(range.start, range.end);
@@ -223,37 +236,42 @@ handle_clear(gcc_unused Client &client, gcc_unused Request args)
 CommandResult
 handle_plchanges(Client &client, Request args)
 {
-	uint32_t version;
+	Response r(client);
 
-	if (!check_uint32(client, &version, args.front()))
+	uint32_t version;
+	if (!ParseCommandArg32(r, version, args.front()))
 		return CommandResult::ERROR;
 
-	playlist_print_changes_info(client, client.playlist, version);
+	playlist_print_changes_info(r, client.partition,
+				    client.playlist, version);
 	return CommandResult::OK;
 }
 
 CommandResult
 handle_plchangesposid(Client &client, Request args)
 {
-	uint32_t version;
+	Response r(client);
 
-	if (!check_uint32(client, &version, args.front()))
+	uint32_t version;
+	if (!ParseCommandArg32(r, version, args.front()))
 		return CommandResult::ERROR;
 
-	playlist_print_changes_position(client, client.playlist, version);
+	playlist_print_changes_position(r, client.playlist, version);
 	return CommandResult::OK;
 }
 
 CommandResult
 handle_playlistinfo(Client &client, Request args)
 {
+	Response r(client);
+
 	RangeArg range = RangeArg::All();
-	if (!args.ParseOptional(0, range, client))
+	if (!args.ParseOptional(0, range, r))
 		return CommandResult::ERROR;
 
-	if (!playlist_print_info(client, client.playlist,
+	if (!playlist_print_info(r, client.partition, client.playlist,
 				 range.start, range.end))
-		return print_playlist_result(client,
+		return print_playlist_result(r,
 					     PlaylistResult::BAD_RANGE);
 
 	return CommandResult::OK;
@@ -262,17 +280,19 @@ handle_playlistinfo(Client &client, Request args)
 CommandResult
 handle_playlistid(Client &client, Request args)
 {
+	Response r(client);
+
 	if (!args.IsEmpty()) {
 		unsigned id;
-		if (!args.Parse(0, id, client))
+		if (!args.Parse(0, id, r))
 			return CommandResult::ERROR;
 
-		bool ret = playlist_print_id(client, client.playlist, id);
+		bool ret = playlist_print_id(r, client.partition,
+					     client.playlist, id);
 		if (!ret)
-			return print_playlist_result(client,
-						     PlaylistResult::NO_SUCH_SONG);
+			return print_playlist_result(r, PlaylistResult::NO_SUCH_SONG);
 	} else {
-		playlist_print_info(client, client.playlist,
+		playlist_print_info(r, client.partition, client.playlist,
 				    0, std::numeric_limits<unsigned>::max());
 	}
 
@@ -283,13 +303,15 @@ static CommandResult
 handle_playlist_match(Client &client, Request args,
 		      bool fold_case)
 {
+	Response r(client);
+
 	SongFilter filter;
 	if (!filter.Parse(args, fold_case)) {
-		command_error(client, ACK_ERROR_ARG, "incorrect arguments");
+		r.Error(ACK_ERROR_ARG, "incorrect arguments");
 		return CommandResult::ERROR;
 	}
 
-	playlist_print_find(client, client.playlist, filter);
+	playlist_print_find(r, client.partition, client.playlist, filter);
 	return CommandResult::OK;
 }
 
@@ -308,13 +330,15 @@ handle_playlistsearch(Client &client, Request args)
 CommandResult
 handle_prio(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned priority;
-	if (!args.ParseShift(0, priority, client, 0xff))
+	if (!args.ParseShift(0, priority, r, 0xff))
 		return CommandResult::ERROR;
 
 	for (const char *i : args) {
 		RangeArg range;
-		if (!ParseCommandArg(client, range, i))
+		if (!ParseCommandArg(r, range, i))
 			return CommandResult::ERROR;
 
 		PlaylistResult result =
@@ -322,7 +346,7 @@ handle_prio(Client &client, Request args)
 							  range.end,
 							  priority);
 		if (result != PlaylistResult::SUCCESS)
-			return print_playlist_result(client, result);
+			return print_playlist_result(r, result);
 	}
 
 	return CommandResult::OK;
@@ -331,19 +355,21 @@ handle_prio(Client &client, Request args)
 CommandResult
 handle_prioid(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned priority;
-	if (!args.ParseShift(0, priority, client, 0xff))
+	if (!args.ParseShift(0, priority, r, 0xff))
 		return CommandResult::ERROR;
 
 	for (const char *i : args) {
 		unsigned song_id;
-		if (!ParseCommandArg(client, song_id, i))
+		if (!ParseCommandArg(r, song_id, i))
 			return CommandResult::ERROR;
 
 		PlaylistResult result =
 			client.partition.SetPriorityId(song_id, priority);
 		if (result != PlaylistResult::SUCCESS)
-			return print_playlist_result(client, result);
+			return print_playlist_result(r, result);
 	}
 
 	return CommandResult::OK;
@@ -352,52 +378,56 @@ handle_prioid(Client &client, Request args)
 CommandResult
 handle_move(Client &client, Request args)
 {
+	Response r(client);
+
 	RangeArg range;
 	int to;
 
-	if (!args.Parse(0, range, client) ||
-	    !args.Parse(1, to, client))
+	if (!args.Parse(0, range, r) || !args.Parse(1, to, r))
 		return CommandResult::ERROR;
 
 	PlaylistResult result =
 		client.partition.MoveRange(range.start, range.end, to);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
 handle_moveid(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned id;
 	int to;
-	if (!args.Parse(0, id, client) ||
-	    !args.Parse(1, to, client))
+	if (!args.Parse(0, id, r) || !args.Parse(1, to, r))
 		return CommandResult::ERROR;
 
 	PlaylistResult result = client.partition.MoveId(id, to);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
 handle_swap(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned song1, song2;
-	if (!args.Parse(0, song1, client) ||
-	    !args.Parse(1, song2, client))
+	if (!args.Parse(0, song1, r) || !args.Parse(1, song2, r))
 		return CommandResult::ERROR;
 
 	PlaylistResult result =
 		client.partition.SwapPositions(song1, song2);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
 
 CommandResult
 handle_swapid(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned id1, id2;
-	if (!args.Parse(0, id1, client) ||
-	    !args.Parse(1, id2, client))
+	if (!args.Parse(0, id1, r) || !args.Parse(1, id2, r))
 		return CommandResult::ERROR;
 
 	PlaylistResult result = client.partition.SwapIds(id1, id2);
-	return print_playlist_result(client, result);
+	return print_playlist_result(r, result);
 }
diff --git a/src/command/Request.hxx b/src/command/Request.hxx
index 38b3c584c..1616b7045 100644
--- a/src/command/Request.hxx
+++ b/src/command/Request.hxx
@@ -28,7 +28,7 @@
 
 #include <assert.h>
 
-class Client;
+class Response;
 
 class Request : public ConstBuffer<const char *> {
 	typedef ConstBuffer<const char *> Base;
@@ -45,26 +45,26 @@ public:
 	}
 
 	template<typename T, typename... Args>
-	bool Parse(unsigned idx, T &value_r, Client &client,
+	bool Parse(unsigned idx, T &value_r, Response &r,
 		   Args&&... args) {
 		assert(idx < size);
 
-		return ParseCommandArg(client, value_r, data[idx],
+		return ParseCommandArg(r, value_r, data[idx],
 				       std::forward<Args>(args)...);
 	}
 
 	template<typename T, typename... Args>
-	bool ParseOptional(unsigned idx, T &value_r, Client &client,
+	bool ParseOptional(unsigned idx, T &value_r, Response &r,
 			   Args&&... args) {
 		return idx >= size ||
-			Parse(idx, value_r, client,
+			Parse(idx, value_r, r,
 			      std::forward<Args>(args)...);
 	}
 
 	template<typename T, typename... Args>
-	bool ParseShift(unsigned idx, T &value_r, Client &client,
+	bool ParseShift(unsigned idx, T &value_r, Response &r,
 			Args&&... args) {
-		bool success = Parse(idx, value_r, client,
+		bool success = Parse(idx, value_r, r,
 				     std::forward<Args>(args)...);
 		shift();
 		return success;
diff --git a/src/command/StickerCommands.cxx b/src/command/StickerCommands.cxx
index a28408547..25d354b2e 100644
--- a/src/command/StickerCommands.cxx
+++ b/src/command/StickerCommands.cxx
@@ -27,8 +27,8 @@
 #include "sticker/StickerPrint.hxx"
 #include "sticker/StickerDatabase.hxx"
 #include "CommandError.hxx"
-#include "protocol/Result.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "Partition.hxx"
 #include "Instance.hxx"
 #include "util/Error.hxx"
@@ -37,7 +37,8 @@
 #include <string.h>
 
 struct sticker_song_find_data {
-	Client &client;
+	Response &r;
+	Partition &partition;
 	const char *name;
 };
 
@@ -48,17 +49,17 @@ sticker_song_find_print_cb(const LightSong &song, const char *value,
 	struct sticker_song_find_data *data =
 		(struct sticker_song_find_data *)user_data;
 
-	song_print_uri(data->client, song);
-	sticker_print_value(data->client, data->name, value);
+	song_print_uri(data->r, data->partition, song);
+	sticker_print_value(data->r, data->name, value);
 }
 
 static CommandResult
-handle_sticker_song(Client &client, Request args)
+handle_sticker_song(Response &r, Partition &partition, Request args)
 {
 	Error error;
-	const Database *db = client.GetDatabase(error);
+	const Database *db = partition.GetDatabase(error);
 	if (db == nullptr)
-		return print_error(client, error);
+		return print_error(r, error);
 
 	const char *const cmd = args.front();
 
@@ -66,53 +67,52 @@ handle_sticker_song(Client &client, Request args)
 	if (args.size == 4 && strcmp(cmd, "get") == 0) {
 		const LightSong *song = db->GetSong(args[2], error);
 		if (song == nullptr)
-			return print_error(client, error);
+			return print_error(r, error);
 
 		const auto value = sticker_song_get_value(*song, args[3],
 							  error);
 		db->ReturnSong(song);
 		if (value.empty()) {
 			if (error.IsDefined())
-				return print_error(client, error);
+				return print_error(r, error);
 
-			command_error(client, ACK_ERROR_NO_EXIST,
-				      "no such sticker");
+			r.Error(ACK_ERROR_NO_EXIST, "no such sticker");
 			return CommandResult::ERROR;
 		}
 
-		sticker_print_value(client, args[3], value.c_str());
+		sticker_print_value(r, args[3], value.c_str());
 
 		return CommandResult::OK;
 	/* list song song_id */
 	} else if (args.size == 3 && strcmp(cmd, "list") == 0) {
 		const LightSong *song = db->GetSong(args[2], error);
 		if (song == nullptr)
-			return print_error(client, error);
+			return print_error(r, error);
 
 		Sticker *sticker = sticker_song_get(*song, error);
 		db->ReturnSong(song);
 		if (sticker) {
-			sticker_print(client, *sticker);
+			sticker_print(r, *sticker);
 			sticker_free(sticker);
 		} else if (error.IsDefined())
-			return print_error(client, error);
+			return print_error(r, error);
 
 		return CommandResult::OK;
 	/* set song song_id id key */
 	} else if (args.size == 5 && strcmp(cmd, "set") == 0) {
 		const LightSong *song = db->GetSong(args[2], error);
 		if (song == nullptr)
-			return print_error(client, error);
+			return print_error(r, error);
 
 		bool ret = sticker_song_set_value(*song, args[3], args[4],
 						  error);
 		db->ReturnSong(song);
 		if (!ret) {
 			if (error.IsDefined())
-				return print_error(client, error);
+				return print_error(r, error);
 
-			command_error(client, ACK_ERROR_SYSTEM,
-				      "failed to set sticker value");
+			r.Error(ACK_ERROR_SYSTEM,
+				"failed to set sticker value");
 			return CommandResult::ERROR;
 		}
 
@@ -122,7 +122,7 @@ handle_sticker_song(Client &client, Request args)
 		   strcmp(cmd, "delete") == 0) {
 		const LightSong *song = db->GetSong(args[2], error);
 		if (song == nullptr)
-			return print_error(client, error);
+			return print_error(r, error);
 
 		bool ret = args.size == 3
 			? sticker_song_delete(*song, error)
@@ -130,10 +130,9 @@ handle_sticker_song(Client &client, Request args)
 		db->ReturnSong(song);
 		if (!ret) {
 			if (error.IsDefined())
-				return print_error(client, error);
+				return print_error(r, error);
 
-			command_error(client, ACK_ERROR_SYSTEM,
-				      "no such sticker");
+			r.Error(ACK_ERROR_SYSTEM, "no such sticker");
 			return CommandResult::ERROR;
 		}
 
@@ -161,14 +160,14 @@ handle_sticker_song(Client &client, Request args)
 			else if (strcmp(op_s, ">") == 0)
 				op = StickerOperator::GREATER_THAN;
 			else {
-				command_error(client, ACK_ERROR_ARG,
-					      "bad operator");
+				r.Error(ACK_ERROR_ARG, "bad operator");
 				return CommandResult::ERROR;
 			}
 		}
 
 		struct sticker_song_find_data data = {
-			client,
+			r,
+			partition,
 			args[3],
 		};
 
@@ -177,16 +176,16 @@ handle_sticker_song(Client &client, Request args)
 				       sticker_song_find_print_cb, &data,
 				       error)) {
 			if (error.IsDefined())
-				return print_error(client, error);
+				return print_error(r, error);
 
-			command_error(client, ACK_ERROR_SYSTEM,
-				      "failed to set search sticker database");
+			r.Error(ACK_ERROR_SYSTEM,
+				"failed to set search sticker database");
 			return CommandResult::ERROR;
 		}
 
 		return CommandResult::OK;
 	} else {
-		command_error(client, ACK_ERROR_ARG, "bad request");
+		r.Error(ACK_ERROR_ARG, "bad request");
 		return CommandResult::ERROR;
 	}
 }
@@ -194,19 +193,19 @@ handle_sticker_song(Client &client, Request args)
 CommandResult
 handle_sticker(Client &client, Request args)
 {
+	Response r(client);
+
 	assert(args.size >= 3);
 
 	if (!sticker_enabled()) {
-		command_error(client, ACK_ERROR_UNKNOWN,
-			      "sticker database is disabled");
+		r.Error(ACK_ERROR_UNKNOWN, "sticker database is disabled");
 		return CommandResult::ERROR;
 	}
 
 	if (strcmp(args[1], "song") == 0)
-		return handle_sticker_song(client, args);
+		return handle_sticker_song(r, client.partition, args);
 	else {
-		command_error(client, ACK_ERROR_ARG,
-			      "unknown sticker domain");
+		r.Error(ACK_ERROR_ARG, "unknown sticker domain");
 		return CommandResult::ERROR;
 	}
 }
diff --git a/src/command/StorageCommands.cxx b/src/command/StorageCommands.cxx
index 3f1e5f321..edf2f60b8 100644
--- a/src/command/StorageCommands.cxx
+++ b/src/command/StorageCommands.cxx
@@ -23,12 +23,12 @@
 #include "StorageCommands.hxx"
 #include "Request.hxx"
 #include "CommandError.hxx"
-#include "protocol/Result.hxx"
 #include "util/UriUtil.hxx"
 #include "util/Error.hxx"
 #include "util/ConstBuffer.hxx"
 #include "fs/Traits.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
 #include "Partition.hxx"
 #include "Instance.hxx"
 #include "storage/Registry.hxx"
@@ -57,7 +57,7 @@ skip_path(const char *name_utf8)
 #endif
 
 static bool
-handle_listfiles_storage(Client &client, StorageDirectoryReader &reader,
+handle_listfiles_storage(Response &r, StorageDirectoryReader &reader,
 			 Error &error)
 {
 	const char *name_utf8;
@@ -75,19 +75,19 @@ handle_listfiles_storage(Client &client, StorageDirectoryReader &reader,
 			continue;
 
 		case StorageFileInfo::Type::REGULAR:
-			client_printf(client, "file: %s\n"
-				      "size: %" PRIu64 "\n",
-				      name_utf8,
-				      info.size);
+			r.Format("file: %s\n"
+				 "size: %" PRIu64 "\n",
+				 name_utf8,
+				 info.size);
 			break;
 
 		case StorageFileInfo::Type::DIRECTORY:
-			client_printf(client, "directory: %s\n", name_utf8);
+			r.Format("directory: %s\n", name_utf8);
 			break;
 		}
 
 		if (info.mtime != 0)
-			time_print(client, "Last-Modified", info.mtime);
+			time_print(r, "Last-Modified", info.mtime);
 	}
 
 	return true;
@@ -98,52 +98,51 @@ handle_listfiles_storage(Client &client, StorageDirectoryReader &reader,
 #endif
 
 static bool
-handle_listfiles_storage(Client &client, Storage &storage, const char *uri,
+handle_listfiles_storage(Response &r, Storage &storage, const char *uri,
 			 Error &error)
 {
 	auto reader = storage.OpenDirectory(uri, error);
 	if (reader == nullptr)
 		return false;
 
-	bool success = handle_listfiles_storage(client, *reader, error);
+	bool success = handle_listfiles_storage(r, *reader, error);
 	delete reader;
 	return success;
 }
 
 CommandResult
-handle_listfiles_storage(Client &client, Storage &storage, const char *uri)
+handle_listfiles_storage(Response &r, Storage &storage, const char *uri)
 {
 	Error error;
-	if (!handle_listfiles_storage(client, storage, uri, error))
-		return print_error(client, error);
+	if (!handle_listfiles_storage(r, storage, uri, error))
+		return print_error(r, error);
 
 	return CommandResult::OK;
 }
 
 CommandResult
-handle_listfiles_storage(Client &client, const char *uri)
+handle_listfiles_storage(Response &r, const char *uri)
 {
 	Error error;
 	Storage *storage = CreateStorageURI(io_thread_get(), uri, error);
 	if (storage == nullptr) {
 		if (error.IsDefined())
-			return print_error(client, error);
+			return print_error(r, error);
 
-		command_error(client, ACK_ERROR_ARG,
-			      "Unrecognized storage URI");
+		r.Error(ACK_ERROR_ARG, "Unrecognized storage URI");
 		return CommandResult::ERROR;
 	}
 
-	bool success = handle_listfiles_storage(client, *storage, "", error);
+	bool success = handle_listfiles_storage(r, *storage, "", error);
 	delete storage;
 	if (!success)
-		return print_error(client, error);
+		return print_error(r, error);
 
 	return CommandResult::OK;
 }
 
 static void
-print_storage_uri(Client &client, const Storage &storage)
+print_storage_uri(Client &client, Response &r, const Storage &storage)
 {
 	std::string uri = storage.MapUTF8("");
 	if (uri.empty())
@@ -165,24 +164,26 @@ print_storage_uri(Client &client, const Storage &storage)
 			uri = std::move(allocated);
 	}
 
-	client_printf(client, "storage: %s\n", uri.c_str());
+	r.Format("storage: %s\n", uri.c_str());
 }
 
 CommandResult
 handle_listmounts(Client &client, gcc_unused Request args)
 {
+	Response r(client);
+
 	Storage *_composite = client.partition.instance.storage;
 	if (_composite == nullptr) {
-		command_error(client, ACK_ERROR_NO_EXIST, "No database");
+		r.Error(ACK_ERROR_NO_EXIST, "No database");
 		return CommandResult::ERROR;
 	}
 
 	CompositeStorage &composite = *(CompositeStorage *)_composite;
 
-	const auto visitor = [&client](const char *mount_uri,
-				       const Storage &storage){
-		client_printf(client, "mount: %s\n", mount_uri);
-		print_storage_uri(client, storage);
+	const auto visitor = [&client, &r](const char *mount_uri,
+					   const Storage &storage){
+		r.Format("mount: %s\n", mount_uri);
+		print_storage_uri(client, r, storage);
 	};
 
 	composite.VisitMounts(visitor);
@@ -193,9 +194,11 @@ handle_listmounts(Client &client, gcc_unused Request args)
 CommandResult
 handle_mount(Client &client, Request args)
 {
+	Response r(client);
+
 	Storage *_composite = client.partition.instance.storage;
 	if (_composite == nullptr) {
-		command_error(client, ACK_ERROR_NO_EXIST, "No database");
+		r.Error(ACK_ERROR_NO_EXIST, "No database");
 		return CommandResult::ERROR;
 	}
 
@@ -205,7 +208,7 @@ handle_mount(Client &client, Request args)
 	const char *const remote_uri = args[1];
 
 	if (*local_uri == 0) {
-		command_error(client, ACK_ERROR_ARG, "Bad mount point");
+		r.Error(ACK_ERROR_ARG, "Bad mount point");
 		return CommandResult::ERROR;
 	}
 
@@ -215,7 +218,7 @@ handle_mount(Client &client, Request args)
 		   UpdateQueue::Erase() really gets called for every
 		   unmount, and no Directory disappears recursively
 		   during database update */
-		command_error(client, ACK_ERROR_ARG, "Bad mount point");
+		r.Error(ACK_ERROR_ARG, "Bad mount point");
 		return CommandResult::ERROR;
 	}
 
@@ -224,10 +227,9 @@ handle_mount(Client &client, Request args)
 					    error);
 	if (storage == nullptr) {
 		if (error.IsDefined())
-			return print_error(client, error);
+			return print_error(r, error);
 
-		command_error(client, ACK_ERROR_ARG,
-			      "Unrecognized storage URI");
+		r.Error(ACK_ERROR_ARG, "Unrecognized storage URI");
 		return CommandResult::ERROR;
 	}
 
@@ -241,7 +243,7 @@ handle_mount(Client &client, Request args)
 
 		if (!db.Mount(local_uri, remote_uri, error)) {
 			composite.Unmount(local_uri);
-			return print_error(client, error);
+			return print_error(r, error);
 		}
 
 		// TODO: call Instance::OnDatabaseModified()?
@@ -256,9 +258,11 @@ handle_mount(Client &client, Request args)
 CommandResult
 handle_unmount(Client &client, Request args)
 {
+	Response r(client);
+
 	Storage *_composite = client.partition.instance.storage;
 	if (_composite == nullptr) {
-		command_error(client, ACK_ERROR_NO_EXIST, "No database");
+		r.Error(ACK_ERROR_NO_EXIST, "No database");
 		return CommandResult::ERROR;
 	}
 
@@ -267,7 +271,7 @@ handle_unmount(Client &client, Request args)
 	const char *const local_uri = args.front();
 
 	if (*local_uri == 0) {
-		command_error(client, ACK_ERROR_ARG, "Bad mount point");
+		r.Error(ACK_ERROR_ARG, "Bad mount point");
 		return CommandResult::ERROR;
 	}
 
@@ -289,7 +293,7 @@ handle_unmount(Client &client, Request args)
 #endif
 
 	if (!composite.Unmount(local_uri)) {
-		command_error(client, ACK_ERROR_ARG, "Not a mount point");
+		r.Error(ACK_ERROR_ARG, "Not a mount point");
 		return CommandResult::ERROR;
 	}
 
diff --git a/src/command/StorageCommands.hxx b/src/command/StorageCommands.hxx
index f5f962042..bbc0149bd 100644
--- a/src/command/StorageCommands.hxx
+++ b/src/command/StorageCommands.hxx
@@ -25,12 +25,13 @@
 class Client;
 class Storage;
 class Request;
+class Response;
 
 CommandResult
-handle_listfiles_storage(Client &client, Storage &storage, const char *uri);
+handle_listfiles_storage(Response &r, Storage &storage, const char *uri);
 
 CommandResult
-handle_listfiles_storage(Client &client, const char *uri);
+handle_listfiles_storage(Response &r, const char *uri);
 
 CommandResult
 handle_listmounts(Client &client, Request args);
diff --git a/src/command/TagCommands.cxx b/src/command/TagCommands.cxx
index 2690f39bf..35efa5859 100644
--- a/src/command/TagCommands.cxx
+++ b/src/command/TagCommands.cxx
@@ -22,7 +22,7 @@
 #include "Request.hxx"
 #include "CommandError.hxx"
 #include "client/Client.hxx"
-#include "protocol/Result.hxx"
+#include "client/Response.hxx"
 #include "tag/Tag.hxx"
 #include "Partition.hxx"
 #include "util/ConstBuffer.hxx"
@@ -30,15 +30,16 @@
 CommandResult
 handle_addtagid(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned song_id;
-	if (!args.Parse(0, song_id, client))
+	if (!args.Parse(0, song_id, r))
 		return CommandResult::ERROR;
 
 	const char *const tag_name = args[1];
 	const TagType tag_type = tag_name_parse_i(tag_name);
 	if (tag_type == TAG_NUM_OF_ITEM_TYPES) {
-		command_error(client, ACK_ERROR_ARG,
-			      "Unknown tag type: %s", tag_name);
+		r.FormatError(ACK_ERROR_ARG, "Unknown tag type: %s", tag_name);
 		return CommandResult::ERROR;
 	}
 
@@ -47,7 +48,7 @@ handle_addtagid(Client &client, Request args)
 	Error error;
 	if (!client.partition.playlist.AddSongIdTag(song_id, tag_type, value,
 						    error))
-		return print_error(client, error);
+		return print_error(r, error);
 
 	return CommandResult::OK;
 }
@@ -55,8 +56,10 @@ handle_addtagid(Client &client, Request args)
 CommandResult
 handle_cleartagid(Client &client, Request args)
 {
+	Response r(client);
+
 	unsigned song_id;
-	if (!args.Parse(0, song_id, client))
+	if (!args.Parse(0, song_id, r))
 		return CommandResult::ERROR;
 
 	TagType tag_type = TAG_NUM_OF_ITEM_TYPES;
@@ -64,7 +67,7 @@ handle_cleartagid(Client &client, Request args)
 		const char *const tag_name = args[1];
 		tag_type = tag_name_parse_i(tag_name);
 		if (tag_type == TAG_NUM_OF_ITEM_TYPES) {
-			command_error(client, ACK_ERROR_ARG,
+			r.FormatError(ACK_ERROR_ARG,
 				      "Unknown tag type: %s", tag_name);
 			return CommandResult::ERROR;
 		}
@@ -73,7 +76,7 @@ handle_cleartagid(Client &client, Request args)
 	Error error;
 	if (!client.partition.playlist.ClearSongIdTag(song_id, tag_type,
 						      error))
-		return print_error(client, error);
+		return print_error(r, error);
 
 	return CommandResult::OK;
 }
diff --git a/src/db/Count.cxx b/src/db/Count.cxx
index 3e974deca..f97f2f288 100644
--- a/src/db/Count.cxx
+++ b/src/db/Count.cxx
@@ -21,7 +21,8 @@
 #include "Count.hxx"
 #include "Selection.hxx"
 #include "Interface.hxx"
-#include "client/Client.hxx"
+#include "Partition.hxx"
+#include "client/Response.hxx"
 #include "LightSong.hxx"
 #include "tag/Tag.hxx"
 
@@ -40,26 +41,24 @@ class TagCountMap : public std::map<std::string, SearchStats> {
 };
 
 static void
-PrintSearchStats(Client &client, const SearchStats &stats)
+PrintSearchStats(Response &r, const SearchStats &stats)
 {
 	unsigned total_duration_s =
 		std::chrono::duration_cast<std::chrono::seconds>(stats.total_duration).count();
 
-	client_printf(client,
-		      "songs: %u\n"
-		      "playtime: %u\n",
-		      stats.n_songs, total_duration_s);
+	r.Format("songs: %u\n"
+		 "playtime: %u\n",
+		 stats.n_songs, total_duration_s);
 }
 
 static void
-Print(Client &client, TagType group, const TagCountMap &m)
+Print(Response &r, TagType group, const TagCountMap &m)
 {
 	assert(unsigned(group) < TAG_NUM_OF_ITEM_TYPES);
 
 	for (const auto &i : m) {
-		client_printf(client, "%s: %s\n",
-			      tag_item_names[group], i.first.c_str());
-		PrintSearchStats(client, i.second);
+		r.Format("%s: %s\n", tag_item_names[group], i.first.c_str());
+		PrintSearchStats(r, i.second);
 	}
 }
 
@@ -109,12 +108,12 @@ GroupCountVisitor(TagCountMap &map, TagType group, const LightSong &song)
 }
 
 bool
-PrintSongCount(Client &client, const char *name,
+PrintSongCount(Response &r, const Partition &partition, const char *name,
 	       const SongFilter *filter,
 	       TagType group,
 	       Error &error)
 {
-	const Database *db = client.GetDatabase(error);
+	const Database *db = partition.GetDatabase(error);
 	if (db == nullptr)
 		return false;
 
@@ -131,7 +130,7 @@ PrintSongCount(Client &client, const char *name,
 		if (!db->Visit(selection, f, error))
 			return false;
 
-		PrintSearchStats(client, stats);
+		PrintSearchStats(r, stats);
 	} else {
 		/* group by the specified tag: store counts in a
 		   std::map */
@@ -144,7 +143,7 @@ PrintSongCount(Client &client, const char *name,
 		if (!db->Visit(selection, f, error))
 			return false;
 
-		Print(client, group, map);
+		Print(r, group, map);
 	}
 
 	return true;
diff --git a/src/db/Count.hxx b/src/db/Count.hxx
index d9f28cb03..4fc26aa7a 100644
--- a/src/db/Count.hxx
+++ b/src/db/Count.hxx
@@ -25,13 +25,14 @@
 #include <stdint.h>
 
 enum TagType : uint8_t;
-class Client;
+struct Partition;
+class Response;
 class SongFilter;
 class Error;
 
-gcc_nonnull(2)
+gcc_nonnull(3)
 bool
-PrintSongCount(Client &client, const char *name,
+PrintSongCount(Response &r, const Partition &partition, const char *name,
 	       const SongFilter *filter,
 	       TagType group,
 	       Error &error);
diff --git a/src/db/DatabasePrint.cxx b/src/db/DatabasePrint.cxx
index 33a4c65fe..3378df23b 100644
--- a/src/db/DatabasePrint.cxx
+++ b/src/db/DatabasePrint.cxx
@@ -24,6 +24,8 @@
 #include "SongPrint.hxx"
 #include "TimePrint.hxx"
 #include "client/Client.hxx"
+#include "client/Response.hxx"
+#include "Partition.hxx"
 #include "tag/Tag.hxx"
 #include "LightSong.hxx"
 #include "LightDirectory.hxx"
@@ -42,116 +44,119 @@ ApplyBaseFlag(const char *uri, bool base)
 }
 
 static void
-PrintDirectoryURI(Client &client, bool base, const LightDirectory &directory)
+PrintDirectoryURI(Response &r, bool base, const LightDirectory &directory)
 {
-	client_printf(client, "directory: %s\n",
-		      ApplyBaseFlag(directory.GetPath(), base));
+	r.Format("directory: %s\n",
+		 ApplyBaseFlag(directory.GetPath(), base));
 }
 
 static bool
-PrintDirectoryBrief(Client &client, bool base, const LightDirectory &directory)
+PrintDirectoryBrief(Response &r, bool base, const LightDirectory &directory)
 {
 	if (!directory.IsRoot())
-		PrintDirectoryURI(client, base, directory);
+		PrintDirectoryURI(r, base, directory);
 
 	return true;
 }
 
 static bool
-PrintDirectoryFull(Client &client, bool base, const LightDirectory &directory)
+PrintDirectoryFull(Response &r, bool base, const LightDirectory &directory)
 {
 	if (!directory.IsRoot()) {
-		PrintDirectoryURI(client, base, directory);
+		PrintDirectoryURI(r, base, directory);
 
 		if (directory.mtime > 0)
-			time_print(client, "Last-Modified", directory.mtime);
+			time_print(r, "Last-Modified", directory.mtime);
 	}
 
 	return true;
 }
 
 static void
-print_playlist_in_directory(Client &client, bool base,
+print_playlist_in_directory(Response &r, bool base,
 			    const char *directory,
 			    const char *name_utf8)
 {
 	if (base || directory == nullptr)
-		client_printf(client, "playlist: %s\n",
-			      ApplyBaseFlag(name_utf8, base));
+		r.Format("playlist: %s\n",
+			 ApplyBaseFlag(name_utf8, base));
 	else
-		client_printf(client, "playlist: %s/%s\n",
-			      directory, name_utf8);
+		r.Format("playlist: %s/%s\n",
+			 directory, name_utf8);
 }
 
 static void
-print_playlist_in_directory(Client &client, bool base,
+print_playlist_in_directory(Response &r, bool base,
 			    const LightDirectory *directory,
 			    const char *name_utf8)
 {
 	if (base || directory == nullptr || directory->IsRoot())
-		client_printf(client, "playlist: %s\n", name_utf8);
+		r.Format("playlist: %s\n", name_utf8);
 	else
-		client_printf(client, "playlist: %s/%s\n",
-			      directory->GetPath(), name_utf8);
+		r.Format("playlist: %s/%s\n",
+			 directory->GetPath(), name_utf8);
 }
 
 static bool
-PrintSongBrief(Client &client, bool base, const LightSong &song)
+PrintSongBrief(Response &r, Partition &partition,
+	       bool base, const LightSong &song)
 {
-	song_print_uri(client, song, base);
+	song_print_uri(r, partition, song, base);
 
 	if (song.tag->has_playlist)
 		/* this song file has an embedded CUE sheet */
-		print_playlist_in_directory(client, base,
+		print_playlist_in_directory(r, base,
 					    song.directory, song.uri);
 
 	return true;
 }
 
 static bool
-PrintSongFull(Client &client, bool base, const LightSong &song)
+PrintSongFull(Response &r, Partition &partition,
+	      bool base, const LightSong &song)
 {
-	song_print_info(client, song, base);
+	song_print_info(r, partition, song, base);
 
 	if (song.tag->has_playlist)
 		/* this song file has an embedded CUE sheet */
-		print_playlist_in_directory(client, base,
+		print_playlist_in_directory(r, base,
 					    song.directory, song.uri);
 
 	return true;
 }
 
 static bool
-PrintPlaylistBrief(Client &client, bool base,
+PrintPlaylistBrief(Response &r, bool base,
 		   const PlaylistInfo &playlist,
 		   const LightDirectory &directory)
 {
-	print_playlist_in_directory(client, base,
+	print_playlist_in_directory(r, base,
 				    &directory, playlist.name.c_str());
 	return true;
 }
 
 static bool
-PrintPlaylistFull(Client &client, bool base,
+PrintPlaylistFull(Response &r, bool base,
 		  const PlaylistInfo &playlist,
 		  const LightDirectory &directory)
 {
-	print_playlist_in_directory(client, base,
+	print_playlist_in_directory(r, base,
 				    &directory, playlist.name.c_str());
 
 	if (playlist.mtime > 0)
-		time_print(client, "Last-Modified", playlist.mtime);
+		time_print(r, "Last-Modified", playlist.mtime);
 
 	return true;
 }
 
 bool
-db_selection_print(Client &client, const DatabaseSelection &selection,
+db_selection_print(Response &r, Partition &partition,
+		   const DatabaseSelection &selection,
 		   bool full, bool base,
 		   unsigned window_start, unsigned window_end,
 		   Error &error)
 {
-	const Database *db = client.GetDatabase(error);
+	const Database *db = partition.GetDatabase(error);
 	if (db == nullptr)
 		return false;
 
@@ -160,13 +165,13 @@ db_selection_print(Client &client, const DatabaseSelection &selection,
 	using namespace std::placeholders;
 	const auto d = selection.filter == nullptr
 		? std::bind(full ? PrintDirectoryFull : PrintDirectoryBrief,
-			    std::ref(client), base, _1)
+			    std::ref(r), base, _1)
 		: VisitDirectory();
 	VisitSong s = std::bind(full ? PrintSongFull : PrintSongBrief,
-				std::ref(client), base, _1);
+				std::ref(r), std::ref(partition), base, _1);
 	const auto p = selection.filter == nullptr
 		? std::bind(full ? PrintPlaylistFull : PrintPlaylistBrief,
-			    std::ref(client), base, _1, _2)
+			    std::ref(r), base, _1, _2)
 		: VisitPlaylist();
 
 	if (window_start > 0 ||
@@ -182,45 +187,47 @@ db_selection_print(Client &client, const DatabaseSelection &selection,
 }
 
 bool
-db_selection_print(Client &client, const DatabaseSelection &selection,
+db_selection_print(Response &r, Partition &partition,
+		   const DatabaseSelection &selection,
 		   bool full, bool base,
 		   Error &error)
 {
-	return db_selection_print(client, selection, full, base,
+	return db_selection_print(r, partition, selection, full, base,
 				  0, std::numeric_limits<int>::max(),
 				  error);
 }
 
 static bool
-PrintSongURIVisitor(Client &client, const LightSong &song)
+PrintSongURIVisitor(Response &r, Partition &partition, const LightSong &song)
 {
-	song_print_uri(client, song);
+	song_print_uri(r, partition, song);
 
 	return true;
 }
 
 static bool
-PrintUniqueTag(Client &client, TagType tag_type,
+PrintUniqueTag(Response &r, TagType tag_type,
 	       const Tag &tag)
 {
 	const char *value = tag.GetValue(tag_type);
 	assert(value != nullptr);
-	client_printf(client, "%s: %s\n", tag_item_names[tag_type], value);
+	r.Format("%s: %s\n", tag_item_names[tag_type], value);
 
 	for (const auto &item : tag)
 		if (item.type != tag_type)
-			client_printf(client, "%s: %s\n",
-				      tag_item_names[item.type], item.value);
+			r.Format("%s: %s\n",
+				 tag_item_names[item.type], item.value);
 
 	return true;
 }
 
 bool
-PrintUniqueTags(Client &client, unsigned type, uint32_t group_mask,
+PrintUniqueTags(Response &r, Partition &partition,
+		unsigned type, uint32_t group_mask,
 		const SongFilter *filter,
 		Error &error)
 {
-	const Database *db = client.GetDatabase(error);
+	const Database *db = partition.GetDatabase(error);
 	if (db == nullptr)
 		return false;
 
@@ -229,13 +236,13 @@ PrintUniqueTags(Client &client, unsigned type, uint32_t group_mask,
 	if (type == LOCATE_TAG_FILE_TYPE) {
 		using namespace std::placeholders;
 		const auto f = std::bind(PrintSongURIVisitor,
-					 std::ref(client), _1);
+					 std::ref(r), std::ref(partition), _1);
 		return db->Visit(selection, f, error);
 	} else {
 		assert(type < TAG_NUM_OF_ITEM_TYPES);
 
 		using namespace std::placeholders;
-		const auto f = std::bind(PrintUniqueTag, std::ref(client),
+		const auto f = std::bind(PrintUniqueTag, std::ref(r),
 					 (TagType)type, _1);
 		return db->VisitUniqueTags(selection, (TagType)type,
 					   group_mask,
diff --git a/src/db/DatabasePrint.hxx b/src/db/DatabasePrint.hxx
index 1c228a507..cf98d31df 100644
--- a/src/db/DatabasePrint.hxx
+++ b/src/db/DatabasePrint.hxx
@@ -26,7 +26,9 @@
 
 class SongFilter;
 struct DatabaseSelection;
+struct Partition;
 class Client;
+class Response;
 class Error;
 
 /**
@@ -34,17 +36,20 @@ class Error;
  * @param base print only base name of songs/directories?
  */
 bool
-db_selection_print(Client &client, const DatabaseSelection &selection,
+db_selection_print(Response &r, Partition &partition,
+		   const DatabaseSelection &selection,
 		   bool full, bool base, Error &error);
 
 bool
-db_selection_print(Client &client, const DatabaseSelection &selection,
+db_selection_print(Response &r, Partition &partition,
+		   const DatabaseSelection &selection,
 		   bool full, bool base,
 		   unsigned window_start, unsigned window_end,
 		   Error &error);
 
 bool
-PrintUniqueTags(Client &client, unsigned type, uint32_t group_mask,
+PrintUniqueTags(Response &r, Partition &partition,
+		unsigned type, uint32_t group_mask,
 		const SongFilter *filter,
 		Error &error);
 
diff --git a/src/decoder/DecoderPrint.cxx b/src/decoder/DecoderPrint.cxx
index fe7206410..6a0822596 100644
--- a/src/decoder/DecoderPrint.cxx
+++ b/src/decoder/DecoderPrint.cxx
@@ -21,35 +21,35 @@
 #include "DecoderPrint.hxx"
 #include "DecoderList.hxx"
 #include "DecoderPlugin.hxx"
-#include "client/Client.hxx"
+#include "client/Response.hxx"
 
 #include <functional>
 
 #include <assert.h>
 
 static void
-decoder_plugin_print(Client &client,
+decoder_plugin_print(Response &r,
 		     const DecoderPlugin &plugin)
 {
 	const char *const*p;
 
 	assert(plugin.name != nullptr);
 
-	client_printf(client, "plugin: %s\n", plugin.name);
+	r.Format("plugin: %s\n", plugin.name);
 
 	if (plugin.suffixes != nullptr)
 		for (p = plugin.suffixes; *p != nullptr; ++p)
-			client_printf(client, "suffix: %s\n", *p);
+			r.Format("suffix: %s\n", *p);
 
 	if (plugin.mime_types != nullptr)
 		for (p = plugin.mime_types; *p != nullptr; ++p)
-			client_printf(client, "mime_type: %s\n", *p);
+			r.Format("mime_type: %s\n", *p);
 }
 
 void
-decoder_list_print(Client &client)
+decoder_list_print(Response &r)
 {
 	using namespace std::placeholders;
-	const auto f = std::bind(decoder_plugin_print, std::ref(client), _1);
+	const auto f = std::bind(decoder_plugin_print, std::ref(r), _1);
 	decoder_plugins_for_each_enabled(f);
 }
diff --git a/src/decoder/DecoderPrint.hxx b/src/decoder/DecoderPrint.hxx
index f8dd03c08..fcb995f6b 100644
--- a/src/decoder/DecoderPrint.hxx
+++ b/src/decoder/DecoderPrint.hxx
@@ -20,9 +20,9 @@
 #ifndef MPD_DECODER_PRINT_HXX
 #define MPD_DECODER_PRINT_HXX
 
-class Client;
+class Response;
 
 void
-decoder_list_print(Client &client);
+decoder_list_print(Response &r);
 
 #endif
diff --git a/src/ls.cxx b/src/ls.cxx
index 0287f5137..41bd4ba97 100644
--- a/src/ls.cxx
+++ b/src/ls.cxx
@@ -19,9 +19,9 @@
 
 #include "config.h"
 #include "ls.hxx"
+#include "client/Response.hxx"
 #include "util/StringUtil.hxx"
 #include "util/UriUtil.hxx"
-#include "client/Client.hxx"
 
 #include <assert.h>
 
@@ -78,12 +78,13 @@ void print_supported_uri_schemes_to_fp(FILE *fp)
 	fprintf(fp,"\n");
 }
 
-void print_supported_uri_schemes(Client &client)
+void
+print_supported_uri_schemes(Response &r)
 {
 	const char *const *prefixes = remoteUrlPrefixes;
 
 	while (*prefixes) {
-		client_printf(client, "handler: %s\n", *prefixes);
+		r.Format("handler: %s\n", *prefixes);
 		prefixes++;
 	}
 }
diff --git a/src/ls.hxx b/src/ls.hxx
index 27843a657..f34e3c6ab 100644
--- a/src/ls.hxx
+++ b/src/ls.hxx
@@ -24,7 +24,7 @@
 
 #include <stdio.h>
 
-class Client;
+class Response;
 
 /**
  * Checks whether the scheme of the specified URI is supported by MPD.
@@ -38,7 +38,7 @@ bool uri_supported_scheme(const char *url);
  * Send a list of supported URI schemes to the client.  This is the
  * response to the "urlhandlers" command.
  */
-void print_supported_uri_schemes(Client &client);
+void print_supported_uri_schemes(Response &r);
 
 /**
  * Send a list of supported URI schemes to a file pointer.
diff --git a/src/output/OutputPrint.cxx b/src/output/OutputPrint.cxx
index 831aea649..d2ddbbf8b 100644
--- a/src/output/OutputPrint.cxx
+++ b/src/output/OutputPrint.cxx
@@ -26,18 +26,17 @@
 #include "OutputPrint.hxx"
 #include "MultipleOutputs.hxx"
 #include "Internal.hxx"
-#include "client/Client.hxx"
+#include "client/Response.hxx"
 
 void
-printAudioDevices(Client &client, const MultipleOutputs &outputs)
+printAudioDevices(Response &r, const MultipleOutputs &outputs)
 {
 	for (unsigned i = 0, n = outputs.Size(); i != n; ++i) {
 		const AudioOutput &ao = outputs.Get(i);
 
-		client_printf(client,
-			      "outputid: %i\n"
-			      "outputname: %s\n"
-			      "outputenabled: %i\n",
-			      i, ao.name, ao.enabled);
+		r.Format("outputid: %i\n"
+			 "outputname: %s\n"
+			 "outputenabled: %i\n",
+			 i, ao.name, ao.enabled);
 	}
 }
diff --git a/src/output/OutputPrint.hxx b/src/output/OutputPrint.hxx
index 2679f6a70..e05c8efd5 100644
--- a/src/output/OutputPrint.hxx
+++ b/src/output/OutputPrint.hxx
@@ -25,10 +25,10 @@
 #ifndef MPD_OUTPUT_PRINT_HXX
 #define MPD_OUTPUT_PRINT_HXX
 
-class Client;
+class Response;
 class MultipleOutputs;
 
 void
-printAudioDevices(Client &client, const MultipleOutputs &outputs);
+printAudioDevices(Response &r, const MultipleOutputs &outputs);
 
 #endif
diff --git a/src/playlist/Print.cxx b/src/playlist/Print.cxx
index 3de9807b7..13e45d160 100644
--- a/src/playlist/Print.cxx
+++ b/src/playlist/Print.cxx
@@ -28,48 +28,51 @@
 #include "fs/Traits.hxx"
 #include "thread/Mutex.hxx"
 #include "thread/Cond.hxx"
-#include "client/Client.hxx"
+#include "Partition.hxx"
+#include "Instance.hxx"
 
 static void
-playlist_provider_print(Client &client, const char *uri,
+playlist_provider_print(Response &r, Partition &partition,
+			const SongLoader &loader,
+			const char *uri,
 			SongEnumerator &e, bool detail)
 {
 	const std::string base_uri = uri != nullptr
 		? PathTraitsUTF8::GetParent(uri)
 		: std::string(".");
 
-	const SongLoader loader(client);
-
 	DetachedSong *song;
 	while ((song = e.NextSong()) != nullptr) {
 		if (playlist_check_translate_song(*song, base_uri.c_str(),
 						  loader) &&
 		    detail)
-			song_print_info(client, *song);
+			song_print_info(r, partition, *song);
 		else
 			/* fallback if no detail was requested or no
 			   detail was available */
-			song_print_uri(client, *song);
+			song_print_uri(r, partition, *song);
 
 		delete song;
 	}
 }
 
 bool
-playlist_file_print(Client &client, const char *uri, bool detail)
+playlist_file_print(Response &r, Partition &partition,
+		    const SongLoader &loader,
+		    const char *uri, bool detail)
 {
 	Mutex mutex;
 	Cond cond;
 
 	SongEnumerator *playlist = playlist_open_any(uri,
 #ifdef ENABLE_DATABASE
-						     client.GetStorage(),
+						     partition.instance.storage,
 #endif
 						     mutex, cond);
 	if (playlist == nullptr)
 		return false;
 
-	playlist_provider_print(client, uri, *playlist, detail);
+	playlist_provider_print(r, partition, loader, uri, *playlist, detail);
 	delete playlist;
 	return true;
 }
diff --git a/src/playlist/Print.hxx b/src/playlist/Print.hxx
index 02096bdc2..3b356d4ce 100644
--- a/src/playlist/Print.hxx
+++ b/src/playlist/Print.hxx
@@ -20,17 +20,20 @@
 #ifndef MPD_PLAYLIST__PRINT_HXX
 #define MPD_PLAYLIST__PRINT_HXX
 
-class Client;
+class Response;
+class SongLoader;
+struct Partition;
 
 /**
  * Send the playlist file to the client.
  *
- * @param client the client which requested the playlist
  * @param uri the URI of the playlist file in UTF-8 encoding
  * @param detail true if all details should be printed
  * @return true on success, false if the playlist does not exist
  */
 bool
-playlist_file_print(Client &client, const char *uri, bool detail);
+playlist_file_print(Response &r, Partition &partition,
+		    const SongLoader &loader,
+		    const char *uri, bool detail);
 
 #endif
diff --git a/src/protocol/ArgParser.cxx b/src/protocol/ArgParser.cxx
index 580d70a20..31756f53e 100644
--- a/src/protocol/ArgParser.cxx
+++ b/src/protocol/ArgParser.cxx
@@ -19,27 +19,26 @@
 
 #include "config.h"
 #include "ArgParser.hxx"
-#include "Result.hxx"
 #include "Chrono.hxx"
+#include "client/Response.hxx"
 
 #include <stdlib.h>
 
 bool
-check_uint32(Client &client, uint32_t *dst, const char *s)
+ParseCommandArg32(Response &r, uint32_t &value_r, const char *s)
 {
 	char *test;
 
-	*dst = strtoul(s, &test, 10);
+	value_r = strtoul(s, &test, 10);
 	if (test == s || *test != '\0') {
-		command_error(client, ACK_ERROR_ARG,
-			      "Integer expected: %s", s);
+		r.FormatError(ACK_ERROR_ARG, "Integer expected: %s", s);
 		return false;
 	}
 	return true;
 }
 
 bool
-ParseCommandArg(Client &client, int &value_r, const char *s,
+ParseCommandArg(Response &r, int &value_r, const char *s,
 		int min_value, int max_value)
 {
 	char *test;
@@ -47,14 +46,12 @@ ParseCommandArg(Client &client, int &value_r, const char *s,
 
 	value = strtol(s, &test, 10);
 	if (test == s || *test != '\0') {
-		command_error(client, ACK_ERROR_ARG,
-			      "Integer expected: %s", s);
+		r.FormatError(ACK_ERROR_ARG, "Integer expected: %s", s);
 		return false;
 	}
 
 	if (value < min_value || value > max_value) {
-		command_error(client, ACK_ERROR_ARG,
-			      "Number too large: %s", s);
+		r.FormatError(ACK_ERROR_ARG, "Number too large: %s", s);
 		return false;
 	}
 
@@ -63,22 +60,22 @@ ParseCommandArg(Client &client, int &value_r, const char *s,
 }
 
 bool
-ParseCommandArg(Client &client, int &value_r, const char *s)
+ParseCommandArg(Response &r, int &value_r, const char *s)
 {
-	return ParseCommandArg(client, value_r, s,
+	return ParseCommandArg(r, value_r, s,
 			       std::numeric_limits<int>::min(),
 			       std::numeric_limits<int>::max());
 }
 
 bool
-ParseCommandArg(Client &client, RangeArg &value_r, const char *s)
+ParseCommandArg(Response &r, RangeArg &value_r, const char *s)
 {
 	char *test, *test2;
 	long value;
 
 	value = strtol(s, &test, 10);
 	if (test == s || (*test != '\0' && *test != ':')) {
-		command_error(client, ACK_ERROR_ARG,
+		r.FormatError(ACK_ERROR_ARG,
 			      "Integer or range expected: %s", s);
 		return false;
 	}
@@ -92,14 +89,12 @@ ParseCommandArg(Client &client, RangeArg &value_r, const char *s)
 	}
 
 	if (value < 0) {
-		command_error(client, ACK_ERROR_ARG,
-			      "Number is negative: %s", s);
+		r.FormatError(ACK_ERROR_ARG, "Number is negative: %s", s);
 		return false;
 	}
 
 	if (unsigned(value) > std::numeric_limits<unsigned>::max()) {
-		command_error(client, ACK_ERROR_ARG,
-			      "Number too large: %s", s);
+		r.FormatError(ACK_ERROR_ARG, "Number too large: %s", s);
 		return false;
 	}
 
@@ -108,7 +103,7 @@ ParseCommandArg(Client &client, RangeArg &value_r, const char *s)
 	if (*test == ':') {
 		value = strtol(++test, &test2, 10);
 		if (*test2 != '\0') {
-			command_error(client, ACK_ERROR_ARG,
+			r.FormatError(ACK_ERROR_ARG,
 				      "Integer or range expected: %s", s);
 			return false;
 		}
@@ -117,13 +112,13 @@ ParseCommandArg(Client &client, RangeArg &value_r, const char *s)
 			value = std::numeric_limits<int>::max();
 
 		if (value < 0) {
-			command_error(client, ACK_ERROR_ARG,
+			r.FormatError(ACK_ERROR_ARG,
 				      "Number is negative: %s", s);
 			return false;
 		}
 
 		if (unsigned(value) > std::numeric_limits<unsigned>::max()) {
-			command_error(client, ACK_ERROR_ARG,
+			r.FormatError(ACK_ERROR_ARG,
 				      "Number too large: %s", s);
 			return false;
 		}
@@ -137,7 +132,7 @@ ParseCommandArg(Client &client, RangeArg &value_r, const char *s)
 }
 
 bool
-ParseCommandArg(Client &client, unsigned &value_r, const char *s,
+ParseCommandArg(Response &r, unsigned &value_r, const char *s,
 		unsigned max_value)
 {
 	unsigned long value;
@@ -145,13 +140,12 @@ ParseCommandArg(Client &client, unsigned &value_r, const char *s,
 
 	value = strtoul(s, &endptr, 10);
 	if (endptr == s || *endptr != 0) {
-		command_error(client, ACK_ERROR_ARG,
-			      "Integer expected: %s", s);
+		r.FormatError(ACK_ERROR_ARG, "Integer expected: %s", s);
 		return false;
 	}
 
 	if (value > max_value) {
-		command_error(client, ACK_ERROR_ARG,
+		r.FormatError(ACK_ERROR_ARG,
 			      "Number too large: %s", s);
 		return false;
 	}
@@ -161,21 +155,21 @@ ParseCommandArg(Client &client, unsigned &value_r, const char *s,
 }
 
 bool
-ParseCommandArg(Client &client, unsigned &value_r, const char *s)
+ParseCommandArg(Response &r, unsigned &value_r, const char *s)
 {
-	return ParseCommandArg(client, value_r, s,
+	return ParseCommandArg(r, value_r, s,
 			       std::numeric_limits<unsigned>::max());
 }
 
 bool
-ParseCommandArg(Client &client, bool &value_r, const char *s)
+ParseCommandArg(Response &r, bool &value_r, const char *s)
 {
 	long value;
 	char *endptr;
 
 	value = strtol(s, &endptr, 10);
 	if (endptr == s || *endptr != 0 || (value != 0 && value != 1)) {
-		command_error(client, ACK_ERROR_ARG,
+		r.FormatError(ACK_ERROR_ARG,
 			      "Boolean (0/1) expected: %s", s);
 		return false;
 	}
@@ -185,15 +179,14 @@ ParseCommandArg(Client &client, bool &value_r, const char *s)
 }
 
 bool
-ParseCommandArg(Client &client, float &value_r, const char *s)
+ParseCommandArg(Response &r, float &value_r, const char *s)
 {
 	float value;
 	char *endptr;
 
 	value = strtof(s, &endptr);
 	if (endptr == s || *endptr != 0) {
-		command_error(client, ACK_ERROR_ARG,
-			      "Float expected: %s", s);
+		r.FormatError(ACK_ERROR_ARG, "Float expected: %s", s);
 		return false;
 	}
 
@@ -202,10 +195,10 @@ ParseCommandArg(Client &client, float &value_r, const char *s)
 }
 
 bool
-ParseCommandArg(Client &client, SongTime &value_r, const char *s)
+ParseCommandArg(Response &r, SongTime &value_r, const char *s)
 {
 	float value;
-	bool success = ParseCommandArg(client, value, s) && value >= 0;
+	bool success = ParseCommandArg(r, value, s) && value >= 0;
 	if (success)
 		value_r = SongTime::FromS(value);
 
@@ -213,10 +206,10 @@ ParseCommandArg(Client &client, SongTime &value_r, const char *s)
 }
 
 bool
-ParseCommandArg(Client &client, SignedSongTime &value_r, const char *s)
+ParseCommandArg(Response &r, SignedSongTime &value_r, const char *s)
 {
 	float value;
-	bool success = ParseCommandArg(client, value, s);
+	bool success = ParseCommandArg(r, value, s);
 	if (success)
 		value_r = SignedSongTime::FromS(value);
 
diff --git a/src/protocol/ArgParser.hxx b/src/protocol/ArgParser.hxx
index b05d7224e..f60dbdf50 100644
--- a/src/protocol/ArgParser.hxx
+++ b/src/protocol/ArgParser.hxx
@@ -26,19 +26,19 @@
 
 #include <stdint.h>
 
-class Client;
+class Response;
 class SongTime;
 class SignedSongTime;
 
 bool
-check_uint32(Client &client, uint32_t *dst, const char *s);
+ParseCommandArg32(Response &r, uint32_t &value_r, const char *s);
 
 bool
-ParseCommandArg(Client &client, int &value_r, const char *s,
+ParseCommandArg(Response &r, int &value_r, const char *s,
 		int min_value, int max_value);
 
 bool
-ParseCommandArg(Client &client, int &value_r, const char *s);
+ParseCommandArg(Response &r, int &value_r, const char *s);
 
 struct RangeArg {
 	unsigned start, end;
@@ -54,25 +54,25 @@ struct RangeArg {
 };
 
 bool
-ParseCommandArg(Client &client, RangeArg &value_r, const char *s);
+ParseCommandArg(Response &r, RangeArg &value_r, const char *s);
 
 bool
-ParseCommandArg(Client &client, unsigned &value_r, const char *s,
+ParseCommandArg(Response &r, unsigned &value_r, const char *s,
 		unsigned max_value);
 
 bool
-ParseCommandArg(Client &client, unsigned &value_r, const char *s);
+ParseCommandArg(Response &r, unsigned &value_r, const char *s);
 
 bool
-ParseCommandArg(Client &client, bool &value_r, const char *s);
+ParseCommandArg(Response &r, bool &value_r, const char *s);
 
 bool
-ParseCommandArg(Client &client, float &value_r, const char *s);
+ParseCommandArg(Response &r, float &value_r, const char *s);
 
 bool
-ParseCommandArg(Client &client, SongTime &value_r, const char *s);
+ParseCommandArg(Response &r, SongTime &value_r, const char *s);
 
 bool
-ParseCommandArg(Client &client, SignedSongTime &value_r, const char *s);
+ParseCommandArg(Response &r, SignedSongTime &value_r, const char *s);
 
 #endif
diff --git a/src/queue/QueuePrint.cxx b/src/queue/QueuePrint.cxx
index c5bda5607..5ae1a3036 100644
--- a/src/queue/QueuePrint.cxx
+++ b/src/queue/QueuePrint.cxx
@@ -22,7 +22,7 @@
 #include "Queue.hxx"
 #include "SongFilter.hxx"
 #include "SongPrint.hxx"
-#include "client/Client.hxx"
+#include "client/Response.hxx"
 
 /**
  * Send detailed information about a range of songs in the queue to a
@@ -33,70 +33,70 @@
  * @param end the index of the last song (excluding)
  */
 static void
-queue_print_song_info(Client &client, const Queue &queue,
+queue_print_song_info(Response &r, Partition &partition, const Queue &queue,
 		      unsigned position)
 {
-	song_print_info(client, queue.Get(position));
-	client_printf(client, "Pos: %u\nId: %u\n",
-		      position, queue.PositionToId(position));
+	song_print_info(r, partition, queue.Get(position));
+	r.Format("Pos: %u\nId: %u\n",
+		 position, queue.PositionToId(position));
 
 	uint8_t priority = queue.GetPriorityAtPosition(position);
 	if (priority != 0)
-		client_printf(client, "Prio: %u\n", priority);
+		r.Format("Prio: %u\n", priority);
 }
 
 void
-queue_print_info(Client &client, const Queue &queue,
+queue_print_info(Response &r, Partition &partition, const Queue &queue,
 		 unsigned start, unsigned end)
 {
 	assert(start <= end);
 	assert(end <= queue.GetLength());
 
 	for (unsigned i = start; i < end; ++i)
-		queue_print_song_info(client, queue, i);
+		queue_print_song_info(r, partition, queue, i);
 }
 
 void
-queue_print_uris(Client &client, const Queue &queue,
+queue_print_uris(Response &r, Partition &partition, const Queue &queue,
 		 unsigned start, unsigned end)
 {
 	assert(start <= end);
 	assert(end <= queue.GetLength());
 
 	for (unsigned i = start; i < end; ++i) {
-		client_printf(client, "%i:", i);
-		song_print_uri(client, queue.Get(i));
+		r.Format("%i:", i);
+		song_print_uri(r, partition, queue.Get(i));
 	}
 }
 
 void
-queue_print_changes_info(Client &client, const Queue &queue,
+queue_print_changes_info(Response &r, Partition &partition, const Queue &queue,
 			 uint32_t version)
 {
 	for (unsigned i = 0; i < queue.GetLength(); i++) {
 		if (queue.IsNewerAtPosition(i, version))
-			queue_print_song_info(client, queue, i);
+			queue_print_song_info(r, partition, queue, i);
 	}
 }
 
 void
-queue_print_changes_position(Client &client, const Queue &queue,
+queue_print_changes_position(Response &r, const Queue &queue,
 			     uint32_t version)
 {
 	for (unsigned i = 0; i < queue.GetLength(); i++)
 		if (queue.IsNewerAtPosition(i, version))
-			client_printf(client, "cpos: %i\nId: %i\n",
-				      i, queue.PositionToId(i));
+			r.Format("cpos: %i\nId: %i\n",
+				 i, queue.PositionToId(i));
 }
 
 void
-queue_find(Client &client, const Queue &queue,
+queue_find(Response &r, Partition &partition, const Queue &queue,
 	   const SongFilter &filter)
 {
 	for (unsigned i = 0; i < queue.GetLength(); i++) {
 		const DetachedSong &song = queue.Get(i);
 
 		if (filter.Match(song))
-			queue_print_song_info(client, queue, i);
+			queue_print_song_info(r, partition, queue, i);
 	}
 }
diff --git a/src/queue/QueuePrint.hxx b/src/queue/QueuePrint.hxx
index 6589ee93e..88d28e8ca 100644
--- a/src/queue/QueuePrint.hxx
+++ b/src/queue/QueuePrint.hxx
@@ -28,27 +28,28 @@
 #include <stdint.h>
 
 struct Queue;
+struct Partition;
 class SongFilter;
-class Client;
+class Response;
 
 void
-queue_print_info(Client &client, const Queue &queue,
+queue_print_info(Response &r, Partition &partition, const Queue &queue,
 		 unsigned start, unsigned end);
 
 void
-queue_print_uris(Client &client, const Queue &queue,
+queue_print_uris(Response &r, Partition &partition, const Queue &queue,
 		 unsigned start, unsigned end);
 
 void
-queue_print_changes_info(Client &client, const Queue &queue,
+queue_print_changes_info(Response &r, Partition &partition, const Queue &queue,
 			 uint32_t version);
 
 void
-queue_print_changes_position(Client &client, const Queue &queue,
+queue_print_changes_position(Response &r, const Queue &queue,
 			     uint32_t version);
 
 void
-queue_find(Client &client, const Queue &queue,
+queue_find(Response &response, Partition &partition, const Queue &queue,
 	   const SongFilter &filter);
 
 #endif
diff --git a/src/sticker/StickerPrint.cxx b/src/sticker/StickerPrint.cxx
index 1682abf77..f0043ebc8 100644
--- a/src/sticker/StickerPrint.cxx
+++ b/src/sticker/StickerPrint.cxx
@@ -20,25 +20,25 @@
 #include "config.h"
 #include "StickerPrint.hxx"
 #include "StickerDatabase.hxx"
-#include "client/Client.hxx"
+#include "client/Response.hxx"
 
 void
-sticker_print_value(Client &client,
+sticker_print_value(Response &r,
 		    const char *name, const char *value)
 {
-	client_printf(client, "sticker: %s=%s\n", name, value);
+	r.Format("sticker: %s=%s\n", name, value);
 }
 
 static void
 print_sticker_cb(const char *name, const char *value, void *data)
 {
-	Client &client = *(Client *)data;
+	auto &r = *(Response *)data;
 
-	sticker_print_value(client, name, value);
+	sticker_print_value(r, name, value);
 }
 
 void
-sticker_print(Client &client, const Sticker &sticker)
+sticker_print(Response &r, const Sticker &sticker)
 {
-	sticker_foreach(sticker, print_sticker_cb, &client);
+	sticker_foreach(sticker, print_sticker_cb, &r);
 }
diff --git a/src/sticker/StickerPrint.hxx b/src/sticker/StickerPrint.hxx
index 53aaca8df..e431245e4 100644
--- a/src/sticker/StickerPrint.hxx
+++ b/src/sticker/StickerPrint.hxx
@@ -21,18 +21,18 @@
 #define MPD_STICKER_PRINT_HXX
 
 struct Sticker;
-class Client;
+class Response;
 
 /**
  * Sends one sticker value to the client.
  */
 void
-sticker_print_value(Client &client, const char *name, const char *value);
+sticker_print_value(Response &r, const char *name, const char *value);
 
 /**
  * Sends all sticker values to the client.
  */
 void
-sticker_print(Client &client, const Sticker &sticker);
+sticker_print(Response &r, const Sticker &sticker);
 
 #endif
diff --git a/test/test_protocol.cxx b/test/test_protocol.cxx
index d5b60323a..457083aee 100644
--- a/test/test_protocol.cxx
+++ b/test/test_protocol.cxx
@@ -1,6 +1,6 @@
 #include "config.h"
 #include "protocol/ArgParser.hxx"
-#include "protocol/Result.hxx"
+#include "client/Response.hxx"
 #include "Compiler.h"
 
 #include <cppunit/TestFixture.h>
@@ -13,10 +13,15 @@
 static enum ack last_error = ack(-1);
 
 void
-command_error(gcc_unused Client &client, enum ack error,
-	      gcc_unused const char *fmt, ...)
+Response::Error(enum ack code, gcc_unused const char *msg)
 {
-	last_error = error;
+	last_error = code;
+}
+
+void
+Response::FormatError(enum ack code, gcc_unused const char *fmt, ...)
+{
+	last_error = code;
 }
 
 class ArgParserTest : public CppUnit::TestFixture {
@@ -32,22 +37,23 @@ void
 ArgParserTest::TestRange()
 {
 	Client &client = *(Client *)nullptr;
+	Response r(client);
 
 	RangeArg range;
 
-	CPPUNIT_ASSERT(ParseCommandArg(client, range, "1"));
+	CPPUNIT_ASSERT(ParseCommandArg(r, range, "1"));
 	CPPUNIT_ASSERT_EQUAL(1u, range.start);
 	CPPUNIT_ASSERT_EQUAL(2u, range.end);
 
-	CPPUNIT_ASSERT(ParseCommandArg(client, range, "1:5"));
+	CPPUNIT_ASSERT(ParseCommandArg(r, range, "1:5"));
 	CPPUNIT_ASSERT_EQUAL(1u, range.start);
 	CPPUNIT_ASSERT_EQUAL(5u, range.end);
 
-	CPPUNIT_ASSERT(ParseCommandArg(client, range, "1:"));
+	CPPUNIT_ASSERT(ParseCommandArg(r, range, "1:"));
 	CPPUNIT_ASSERT_EQUAL(1u, range.start);
 	CPPUNIT_ASSERT(range.end >= 999999u);
 
-	CPPUNIT_ASSERT(!ParseCommandArg(client, range, "-2"));
+	CPPUNIT_ASSERT(!ParseCommandArg(r, range, "-2"));
 	CPPUNIT_ASSERT_EQUAL(ACK_ERROR_ARG, last_error);
 }