]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
auth: Add /api/docs endpoint to obtain OpenAPI document 8911/head
authorKevin P. Fleming <kevin@km6g.us>
Tue, 10 Nov 2020 12:29:39 +0000 (07:29 -0500)
committerKevin P. Fleming <kevin@km6g.us>
Tue, 10 Nov 2020 18:34:47 +0000 (13:34 -0500)
This patch adds an /api/docs endpoint to the API webserver,
allowing clients to obtain the OpenAPI (Swagger) document that
describes the server's API directly from the server.

It also modifies the response body mechanism in the webserver
to no longer assume JSON output, but allow handlers to specify
JSON, YAML, or plain text. It also adds detection of YAML
support in the request so that handlers can choose which type
to send in their response. Since there is not yet a standard
MIME type for YAML, 'application/x-yaml' is used since it
appears to be the most commonly used type.

Signed-off-by: Kevin P. Fleming <kevin@km6g.us>
17 files changed:
.github/actions/spell-check/expect.txt
.github/workflows/codeql-analysis.yml
docs/http-api/index.rst
docs/http-api/swagger/authoritative-api-swagger.yaml
ext/Makefile.am
lgtm.yml
m4/pdns_check_virtualenv.m4
pdns/.gitignore
pdns/Makefile.am
pdns/incfiles [new file with mode: 0755]
pdns/requirements.txt [new file with mode: 0644]
pdns/webserver.cc
pdns/webserver.hh
pdns/ws-api.cc
pdns/ws-auth.cc
pdns/ws-auth.hh
pdns/ws-recursor.cc

index 6199044115f1bd58adf33d0eb2fc37ac4af92d18..8eb330a5e9f7c765f3a9188a4d98c702c16403fe 100644 (file)
@@ -69,6 +69,7 @@ ANSSI
 Antoin
 anycast
 api
+apidocfiles
 apikey
 APIv
 AQAB
index a1ed8831f180eaa787775513eb72c1c66d6d9982..d31152a37d9e4ecc7f196cb7bd77f5bfe5a2ea94 100644 (file)
@@ -33,6 +33,12 @@ jobs:
     - run: git checkout HEAD^2
       if: ${{ github.event_name == 'pull_request' }}
 
+    # Python and virtualenv are required for building the Authoritative server
+    - uses: actions/setup-python@v2
+      with:
+        python-version: '2.7'
+    - run: pip install virtualenv
+
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v1
index 21b369c94ab42d456b9a8664e9229da9c6645a53..b7e01df683ecf9e1b7587da3323f46c17109f7e2 100644 (file)
@@ -318,7 +318,9 @@ Working with the API
 
 This chapter describes the PowerDNS Authoritative API.
 When creating an API wrapper (for instance when fronting multiple API's), it is recommended to stick to this API specification.
-The API is described in the `OpenAPI format <https://www.openapis.org/>`_, also known as "Swagger", and this description is `available <https://raw.githubusercontent.com/PowerDNS/pdns/master/docs/http-api/swagger/authoritative-api-swagger.yaml>`_.
+The API is described in the `OpenAPI format <https://www.openapis.org/>`_, also known as "Swagger", and this description is `available <https://raw.githubusercontent.com/PowerDNS/pdns/master/docs/http-api/swagger/authoritative-api-swagger.yaml>`_. It can also be obtained from a running server if the administrator of that server has enabled the API; it
+is available at the  `/api/docs` endpoint in both YAML and JSON formats (the 'Accept' header can be used to indicate the
+desired format).
 
 Authentication
 ~~~~~~~~~~~~~~
index 42b0a431f2dfd7b04ce8cc1459417470e2b79968..1fa5c67cf51898ef8a1b24c75bc4bb87c9b61c73 100644 (file)
@@ -4,10 +4,7 @@ info:
   title: PowerDNS Authoritative HTTP API
   license:
     name: MIT
-host: localhost:8081
 basePath: /api/v1
-schemes:
-  - http
 consumes:
   - application/json
 produces:
@@ -406,9 +403,9 @@ paths:
           schema:
             type: array
             items:
-            - $ref: '#/definitions/StatisticItem'
-            - $ref: '#/definitions/MapStatisticItem'
-            - $ref: '#/definitions/RingStatisticItem'
+              - $ref: '#/definitions/StatisticItem'
+              - $ref: '#/definitions/MapStatisticItem'
+              - $ref: '#/definitions/RingStatisticItem'
         '422':
           description: 'Returned when a non-existing statistic name has been requested. Contains an error message'
 
index 7c0a42d4199fc6feaaaf3168da99b1e499b1e689..1a99b603869aeef7907938b5c50f15f4a8430222 100644 (file)
@@ -9,4 +9,5 @@ DIST_SUBDIRS = \
        yahttp
 
 EXTRA_DIST = \
-       luawrapper/include/LuaContext.hpp
+       luawrapper/include/LuaContext.hpp \
+       incbin/incbin.h
index a78d90d5e36dd6a9abe7831b32ea801601904195..67929165e2586cf814b823bddcfb3a1d28cfdf93 100644 (file)
--- a/lgtm.yml
+++ b/lgtm.yml
@@ -23,6 +23,9 @@ extraction:
       - "sed -i 's/codedocs docs//' Makefile.am"
       - "autoreconf -vi"
       - "./configure --with-modules='bind gsqlite3 gmysql gpgsql ldap'"
+      - "pushd pdns"
+      - "touch .venv api-swagger.yaml api-swagger.json"
+      - "popd"
     index:
       build_command:
       - "pushd pdns/dnsdistdist"
index 498cff7737c1e9a2a297c1fbcc106d765f984311..873cde5df00f63b0d9ebbff567e562c367667c0e 100644 (file)
@@ -8,5 +8,5 @@ AC_DEFUN([PDNS_CHECK_VIRTUALENV], [
   ])
   AM_CONDITIONAL([HAVE_VIRTUALENV], [test "x$VIRTUALENV" != "xno"])
   AM_CONDITIONAL([HAVE_MANPAGES], [test -e "$srcdir/docs/pdns_server.1"])
+  AM_CONDITIONAL([HAVE_API_SWAGGER_JSON], [test -e "$srcdir/pdns/api-swagger.json"])
 ])
-
index 41e738c99468f0cabd7bb32aee551c938e000ca1..7b6406ec349b6482b6a1deb230d670d80415f40b 100644 (file)
@@ -55,7 +55,10 @@ version_generated.h
 /pubsuffix.cc
 /calidns
 /dumresp
-/htmlfiles.h
+/apidocfiles.h
+/api-swagger.yaml
+/api-swagger.json
+/.venv
 effective_tld_names.dat
 /dnsmessage.pb.cc
 /dnsmessage.pb.h
index 3ad600c2a8b33bc6306d92cf6c3936c8f9eb9be0..d85f1d7050c1bddcc69abfd18ea1fca516a6bca5 100644 (file)
@@ -50,12 +50,17 @@ EXTRA_DIST = \
        ixfrdist.example.yml \
        lua-record.cc \
        minicurl.cc \
-       minicurl.hh
+       minicurl.hh \
+       api-swagger.yaml \
+       api-swagger.json \
+       requirements.txt \
+       incfiles
 
 BUILT_SOURCES = \
        bind-dnssec.schema.sqlite3.sql.h \
        bindparser.h \
-       dnslabeltext.cc
+       dnslabeltext.cc \
+       apidocfiles.h
 
 CLEANFILES = \
        *.gcda \
@@ -65,7 +70,34 @@ CLEANFILES = \
        backends/gsql/gsqlbackend.gcno \
        backends/gsql/gsqlbackend.gcov \
        dnsmessage.pb.cc dnsmessage.pb.h \
-       pdns.conf-dist
+       pdns.conf-dist \
+       apidocfiles.h \
+       api-swagger.yaml \
+       api-swagger.json
+
+# use a $(wildcard) wrapper here to allow build to proceed if output
+# file is present but input file is not (e.g. in a dist tarball)
+api-swagger.yaml: $(wildcard ../docs/http-api/swagger/authoritative-api-swagger.yaml)
+       cp $< $@
+
+if HAVE_VIRTUALENV
+.venv: requirements.txt
+       virtualenv .venv
+       .venv/bin/pip install -U pip setuptools setuptools-git
+       .venv/bin/pip install -r requirements.txt
+
+api-swagger.json: api-swagger.yaml .venv
+       .venv/bin/python -c "import sys, json, yaml; y = yaml.safe_load(sys.stdin.read()); json.dump(y, sys.stdout, indent=2, separators=(',', ': '))" < $< > $@
+else # if HAVE_VIRTUALENV
+if !HAVE_API_SWAGGER_JSON
+api-swagger.json:
+       echo "You need virtualenv to generate the JSON API document"
+       exit 1
+endif
+endif
+
+apidocfiles.h: api-swagger.yaml api-swagger.json
+       ./incfiles $^ > $@
 
 noinst_SCRIPTS = pdns.init
 sysconf_DATA = pdns.conf-dist
diff --git a/pdns/incfiles b/pdns/incfiles
new file mode 100755 (executable)
index 0000000..c2a99c0
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+export LC_ALL=C.UTF-8
+export LANG=C.UTF-8
+
+for a in $@
+do
+       c=$(echo $a | tr "/.-" "___")
+       echo "INCBIN(${c}, \"${a}\");"
+       echo "static const string g_${c}{(const char*)g${c}Data, g${c}Size};"
+done
diff --git a/pdns/requirements.txt b/pdns/requirements.txt
new file mode 100644 (file)
index 0000000..5500f00
--- /dev/null
@@ -0,0 +1 @@
+PyYAML
index ca10668787c54e0110bc5de5b000134a55150d48..252506da0f99af8e12af3841b5c765c3003631a8 100644 (file)
@@ -81,21 +81,43 @@ bool HttpRequest::compareHeader(const string &header_name, const string &expecte
   return (0==strcmp(header->second.c_str(), expected_value.c_str()));
 }
 
+void HttpResponse::setPlainBody(const string& document)
+{
+  this->headers["Content-Type"] = "text/plain; charset=utf-8";
+
+  this->body = document;
+}
 
-void HttpResponse::setBody(const json11::Json& document)
+void HttpResponse::setYamlBody(const string& document)
 {
+  this->headers["Content-Type"] = "application/x-yaml";
+
+  this->body = document;
+}
+
+void HttpResponse::setJsonBody(const string& document)
+{
+  this->headers["Content-Type"] = "application/json";
+
+  this->body = document;
+}
+
+void HttpResponse::setJsonBody(const json11::Json& document)
+{
+  this->headers["Content-Type"] = "application/json";
+
   document.dump(this->body);
 }
 
 void HttpResponse::setErrorResult(const std::string& message, const int status_)
 {
-  setBody(json11::Json::object { { "error", message } });
+  setJsonBody(json11::Json::object { { "error", message } });
   this->status = status_;
 }
 
 void HttpResponse::setSuccessResult(const std::string& message, const int status_)
 {
-  setBody(json11::Json::object { { "result", message } });
+  setJsonBody(json11::Json::object { { "result", message } });
   this->status = status_;
 }
 
@@ -150,8 +172,6 @@ void WebServer::apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req,
     throw HttpUnauthorizedException("X-API-Key");
   }
 
-  resp->headers["Content-Type"] = "application/json";
-
   // security headers
   resp->headers["X-Content-Type-Options"] = "nosniff";
   resp->headers["X-Frame-Options"] = "deny";
@@ -233,8 +253,12 @@ void WebServer::handleRequest(HttpRequest& req, HttpResponse& resp) const
     YaHTTP::strstr_map_t::iterator header;
 
     if ((header = req.headers.find("accept")) != req.headers.end()) {
-      // json wins over html
-      if (header->second.find("application/json") != std::string::npos) {
+      // yaml wins over json, json wins over html
+      if (header->second.find("application/x-yaml") != std::string::npos) {
+        req.accept_yaml = true;
+      } else if (header->second.find("text/x-yaml") != std::string::npos) {
+        req.accept_yaml = true;
+      } else if (header->second.find("application/json") != std::string::npos) {
         req.accept_json = true;
       } else if (header->second.find("text/html") != std::string::npos) {
         req.accept_html = true;
@@ -272,14 +296,14 @@ void WebServer::handleRequest(HttpRequest& req, HttpResponse& resp) const
     // TODO rm this logline?
     g_log<<Logger::Debug<<req.logprefix<<"Error result for \"" << req.url.path << "\": " << resp.status << endl;
     string what = YaHTTP::Utility::status2text(resp.status);
-    if(req.accept_html) {
-      resp.headers["Content-Type"] = "text/html; charset=utf-8";
-      resp.body = "<!html><title>" + what + "</title><h1>" + what + "</h1>";
-    } else if (req.accept_json) {
+    if (req.accept_json) {
       resp.headers["Content-Type"] = "application/json";
       if (resp.body.empty()) {
         resp.setErrorResult(what, resp.status);
       }
+    } else if (req.accept_html) {
+      resp.headers["Content-Type"] = "text/html; charset=utf-8";
+      resp.body = "<!html><title>" + what + "</title><h1>" + what + "</h1>";
     } else {
       resp.headers["Content-Type"] = "text/plain; charset=utf-8";
       resp.body = what;
index ea98581c93f26e1d0b521a3510ce3e05d81a64d3..9e1f67971ec5048a3830978f1ea928473995c917 100644 (file)
@@ -33,6 +33,7 @@ class HttpRequest : public YaHTTP::Request {
 public:
   HttpRequest(const string& logprefix_="") : YaHTTP::Request(), accept_json(false), accept_html(false), complete(false), logprefix(logprefix_) { };
 
+  bool accept_yaml;
   bool accept_json;
   bool accept_html;
   bool complete;
@@ -49,7 +50,10 @@ public:
   HttpResponse() : YaHTTP::Response() { };
   HttpResponse(const YaHTTP::Response &resp) : YaHTTP::Response(resp) { };
 
-  void setBody(const json11::Json& document);
+  void setPlainBody(const string& document);
+  void setYamlBody(const string& document);
+  void setJsonBody(const string& document);
+  void setJsonBody(const json11::Json& document);
   void setErrorResult(const std::string& message, const int status);
   void setSuccessResult(const std::string& message, const int status = 200);
 };
index 64c87efa33a189e48a521689d62773a72c07be45..bae04278f62ec4e663066220a13bea1112271105 100644 (file)
@@ -114,7 +114,7 @@ void apiDiscovery(HttpRequest* req, HttpResponse* resp) {
   };
   Json doc = Json::array { version1 };
 
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 void apiServer(HttpRequest* req, HttpResponse* resp) {
@@ -122,14 +122,14 @@ void apiServer(HttpRequest* req, HttpResponse* resp) {
     throw HttpMethodNotAllowedException();
 
   Json doc = Json::array {getServerDetail()};
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 void apiServerDetail(HttpRequest* req, HttpResponse* resp) {
   if(req->method != "GET")
     throw HttpMethodNotAllowedException();
 
-  resp->setBody(getServerDetail());
+  resp->setJsonBody(getServerDetail());
 }
 
 void apiServerConfig(HttpRequest* req, HttpResponse* resp) {
@@ -151,7 +151,7 @@ void apiServerConfig(HttpRequest* req, HttpResponse* resp) {
       { "value", value },
     });
   }
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 void apiServerStatistics(HttpRequest* req, HttpResponse* resp) {
@@ -172,7 +172,7 @@ void apiServerStatistics(HttpRequest* req, HttpResponse* resp) {
       { "value", std::to_string(*stat) },
     });
 
-    resp->setBody(doc);
+    resp->setJsonBody(doc);
 
     return;
   }
@@ -273,7 +273,7 @@ void apiServerStatistics(HttpRequest* req, HttpResponse* resp) {
   }
 #endif
 
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 DNSName apiNameToDNSName(const string& name) {
index eafe939f7a2a8c5b0676f47d262ac427fd4e2f14..df0124f09b472d97a6f55e06fe7d2b3af9c6765b 100644 (file)
@@ -47,6 +47,7 @@
 #include "auth-caches.hh"
 #include "threadname.hh"
 #include "tsigutils.hh"
+#include "ext/incbin/incbin.h"
 
 using json11::Json;
 
@@ -486,7 +487,7 @@ static void fillZone(UeberBackend& B, const DNSName& zonename, HttpResponse* res
     doc["rrsets"] = rrsets;
   }
 
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 void productServerStatisticsFetch(map<string,string>& out)
@@ -882,6 +883,23 @@ static bool isValidMetadataKind(const string& kind, bool readonly) {
   return found;
 }
 
+/* Return OpenAPI document describing the supported API.
+ */
+#include "apidocfiles.h"
+
+void apiDocs(HttpRequest* req, HttpResponse* resp) {
+  if(req->method != "GET")
+    throw HttpMethodNotAllowedException();
+
+  if (req->accept_yaml) {
+    resp->setYamlBody(g_api_swagger_yaml);
+  } else if (req->accept_json) {
+    resp->setJsonBody(g_api_swagger_json);
+  } else {
+    resp->setPlainBody(g_api_swagger_yaml);
+  }
+}
+
 static void apiZoneMetadata(HttpRequest* req, HttpResponse *resp) {
   DNSName zonename = apiZoneIdToName(req->parameters["id"]);
 
@@ -912,7 +930,7 @@ static void apiZoneMetadata(HttpRequest* req, HttpResponse *resp) {
       document.push_back(key);
     }
 
-    resp->setBody(document);
+    resp->setJsonBody(document);
   } else if (req->method == "POST") {
     auto document = req->json();
     string kind;
@@ -962,7 +980,7 @@ static void apiZoneMetadata(HttpRequest* req, HttpResponse *resp) {
     };
 
     resp->status = 201;
-    resp->setBody(key);
+    resp->setJsonBody(key);
   } else
     throw HttpMethodNotAllowedException();
 }
@@ -995,7 +1013,7 @@ static void apiZoneMetadataKind(HttpRequest* req, HttpResponse* resp) {
       entries.push_back(i);
 
     document["metadata"] = entries;
-    resp->setBody(document);
+    resp->setJsonBody(document);
   } else if (req->method == "PUT") {
     auto document = req->json();
 
@@ -1022,7 +1040,7 @@ static void apiZoneMetadataKind(HttpRequest* req, HttpResponse* resp) {
       { "metadata", metadata }
     };
 
-    resp->setBody(key);
+    resp->setJsonBody(key);
   } else if (req->method == "DELETE") {
     if (!isValidMetadataKind(kind, false))
       throw ApiException("Unsupported metadata kind '" + kind + "'");
@@ -1090,7 +1108,7 @@ static void apiZoneCryptokeysGET(DNSName zonename, int inquireKeyId, HttpRespons
 
     if (inquireSingleKey) {
       key["privatekey"] = value.first.getKey()->convertToISC();
-      resp->setBody(key);
+      resp->setJsonBody(key);
       return;
     }
     doc.push_back(key);
@@ -1100,7 +1118,7 @@ static void apiZoneCryptokeysGET(DNSName zonename, int inquireKeyId, HttpRespons
     // we came here because we couldn't find the requested key.
     throw HttpNotFoundException();
   }
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 
 }
 
@@ -1452,7 +1470,7 @@ static void apiServerTSIGKeys(HttpRequest* req, HttpResponse* resp) {
     for(const auto &key : keys) {
       doc.push_back(makeJSONTSIGKey(key, false));
     }
-    resp->setBody(doc);
+    resp->setJsonBody(doc);
   } else if (req->method == "POST") {
     auto document = req->json();
     DNSName keyname(stringFromJson(document, "name"));
@@ -1475,7 +1493,7 @@ static void apiServerTSIGKeys(HttpRequest* req, HttpResponse* resp) {
     }
 
     resp->status = 201;
-    resp->setBody(makeJSONTSIGKey(keyname, algo, content));
+    resp->setJsonBody(makeJSONTSIGKey(keyname, algo, content));
   } else {
     throw HttpMethodNotAllowedException();
   }
@@ -1497,7 +1515,7 @@ static void apiServerTSIGKeyDetail(HttpRequest* req, HttpResponse* resp) {
   tsk.key = content;
 
   if (req->method == "GET") {
-    resp->setBody(makeJSONTSIGKey(tsk));
+    resp->setJsonBody(makeJSONTSIGKey(tsk));
   } else if (req->method == "PUT") {
     json11::Json document;
     if (!req->body.empty()) {
@@ -1531,7 +1549,7 @@ static void apiServerTSIGKeyDetail(HttpRequest* req, HttpResponse* resp) {
         throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
       }
     }
-    resp->setBody(makeJSONTSIGKey(tsk));
+    resp->setJsonBody(makeJSONTSIGKey(tsk));
   } else if (req->method == "DELETE") {
     if (!B.deleteTSIGKey(keyname)) {
       throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
@@ -1747,7 +1765,7 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
   for(const DomainInfo& di : domains) {
     doc.push_back(getZoneInfo(di, with_dnssec ? &dk : nullptr));
   }
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
@@ -1828,7 +1846,7 @@ static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
   }
 
   if (req->accept_json) {
-    resp->setBody(Json::object { { "zone", ss.str() } });
+    resp->setJsonBody(Json::object { { "zone", ss.str() } });
   } else {
     resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
     resp->body = ss.str();
@@ -2190,7 +2208,7 @@ static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
     }
   }
 
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
@@ -2200,7 +2218,7 @@ static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
   DNSName canon = apiNameToDNSName(req->getvars["domain"]);
 
   uint64_t count = purgeAuthCachesExact(canon);
-  resp->setBody(Json::object {
+  resp->setJsonBody(Json::object {
       { "count", (int) count },
       { "result", "Flushed cache." }
   });
@@ -2298,6 +2316,7 @@ void AuthWebServer::webThread()
       d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
       d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
       d_ws->registerApiHandler("/api/v1/servers", &apiServer);
+      d_ws->registerApiHandler("/api/docs", &apiDocs);
       d_ws->registerApiHandler("/api", &apiDiscovery);
     }
     if (::arg().mustDo("webserver")) {
index 2cd127345fadae064a349f2585dc6c61669f5c70..8c84360974dea7d35c2af883e095b75c15ad0969 100644 (file)
@@ -94,3 +94,5 @@ private:
   Ewma d_qcachehits, d_qcachemisses;
   WebServer *d_ws{nullptr};
 };
+
+void apiDocs(HttpRequest* req, HttpResponse* resp);
index 68feb47e1ac469c1cc07b5c22b9f4d7ead966cf9..e301cf7e07729c6a6cda3fd2070c60b608e55fad 100644 (file)
@@ -116,7 +116,7 @@ static void apiServerConfigAllowFrom(HttpRequest* req, HttpResponse* resp)
   vector<string> entries;
   t_allowFrom->toStringVector(&entries);
 
-  resp->setBody(Json::object {
+  resp->setJsonBody(Json::object {
     { "name", "allow-from" },
     { "value", entries },
   });
@@ -157,7 +157,7 @@ static void fillZone(const DNSName& zonename, HttpResponse* resp)
     { "records", records }
   };
 
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 static void doCreateZone(const Json document)
@@ -296,7 +296,7 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp)
       { "recursion_desired", zone.d_servers.empty() ? false : zone.d_rdForward }
     });
   }
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
@@ -372,7 +372,7 @@ static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
       });
     }
   }
-  resp->setBody(doc);
+  resp->setJsonBody(doc);
 }
 
 static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
@@ -389,7 +389,7 @@ static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
   int count = g_recCache->doWipeCache(canon, subtree, qtype);
   count += broadcastAccFunction<uint64_t>([=]{return pleaseWipePacketCache(canon, subtree, qtype);});
   count += g_negCache->wipe(canon, subtree);
-  resp->setBody(Json::object {
+  resp->setJsonBody(Json::object {
     { "count", count },
     { "result", "Flushed cache." }
   });
@@ -422,7 +422,7 @@ static void apiServerRPZStats(HttpRequest* req, HttpResponse* resp) {
     };
     ret[name] = zoneInfo;
   }
-  resp->setBody(ret);
+  resp->setJsonBody(ret);
 }
 
 
@@ -594,7 +594,7 @@ void RecursorWebServer::jsonstat(HttpRequest* req, HttpResponse *resp)
         (int)(queries.size() - totIncluded), "", ""
       });
     }
-    resp->setBody(Json::object { { "entries", entries } });
+    resp->setJsonBody(Json::object { { "entries", entries } });
     return;
   }
   else if(command == "get-remote-ring") {
@@ -640,7 +640,7 @@ void RecursorWebServer::jsonstat(HttpRequest* req, HttpResponse *resp)
       });
     }
 
-    resp->setBody(Json::object { { "entries", entries } });
+    resp->setJsonBody(Json::object { { "entries", entries } });
     return;
   } else {
     resp->setErrorResult("Command '"+command+"' not found", 404);