(PE-6019) Encode string request body per Content-Type header

This commit encodes a request body string per the content of the
Content-Type header.  In the event that no charset is specified in the
Content-Type header, the charset is set to UTF-8.  Previously, the
request body string was always encoded to ISO-8859-1 and was not
necessarily in sync with the value of the Content-Type header.
This commit is contained in:
Jeremy Barlow 2014-09-18 10:59:35 -07:00
parent 611218fb23
commit f83d7821ad
3 changed files with 178 additions and 24 deletions

View file

@ -19,7 +19,7 @@
(org.apache.http.client.utils URIBuilder)
(org.apache.http.concurrent FutureCallback)
(org.apache.http.message BasicHeader)
(org.apache.http Header)
(org.apache.http Consts Header)
(org.apache.http.nio.entity NStringEntity)
(org.apache.http.entity InputStreamEntity ContentType)
(java.io InputStream)
@ -82,17 +82,30 @@
[decompress-body? headers]
(if (and decompress-body?
(not (contains? headers "accept-encoding")))
(assoc headers "accept-encoding" (BasicHeader. "accept-encoding" "gzip, deflate"))
(assoc headers "accept-encoding"
(BasicHeader. "Accept-Encoding" "gzip, deflate"))
headers))
(defn- add-content-type-header
[content-type headers]
(if content-type
(assoc headers "content-type" (BasicHeader. "Content-Type"
(str (.getMimeType content-type)
"; charset="
(-> content-type
.getCharset
.name))))
headers))
(defn- prepare-headers
[{:keys [headers decompress-body]}]
[{:keys [headers decompress-body]} content-type]
(->> headers
(reduce
(fn [acc [k v]]
(assoc acc (str/lower-case k) (BasicHeader. k v)))
{})
(add-accept-encoding-header decompress-body)
(add-content-type-header content-type)
vals
(into-array Header)))
@ -105,14 +118,28 @@
query-params)]
(.build uri-builder))))
(defn- content-type
[{:keys [headers]}]
(if-let [content-type-value (some #(when (= "content-type"
(clojure.string/lower-case (key %)))
(val %))
headers)]
(let [content-type (ContentType/parse content-type-value)]
(if (.getCharset content-type)
content-type
(ContentType/create (.getMimeType content-type) Consts/UTF_8)))))
(defn- coerce-opts
[{:keys [url body query-params] :as opts}]
(let [url (parse-url url query-params)]
(let [url (parse-url url query-params)
content-type (content-type opts)]
{:url url
:method (clojure.core/get opts :method :get)
:headers (prepare-headers opts)
:headers (prepare-headers opts content-type)
:body (cond
(string? body) (NStringEntity. body)
(string? body) (if content-type
(NStringEntity. body content-type)
(NStringEntity. body))
(instance? InputStream body) (InputStreamEntity. body)
:else body)}))

View file

@ -23,6 +23,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
@ -34,7 +36,8 @@ public class JavaClient {
private static final String PROTOCOL = "TLS";
private static Header[] prepareHeaders(RequestOptions options) {
private static Header[] prepareHeaders(RequestOptions options,
ContentType contentType) {
Map<String, Header> result = new HashMap<String, Header>();
Map<String, String> origHeaders = options.getHeaders();
if (origHeaders == null) {
@ -44,12 +47,52 @@ public class JavaClient {
result.put(entry.getKey().toLowerCase(), new BasicHeader(entry.getKey(), entry.getValue()));
}
if (options.getDecompressBody() &&
(! origHeaders.containsKey("accept-encoding"))) {
(! result.containsKey("accept-encoding"))) {
result.put("accept-encoding", new BasicHeader("Accept-Encoding", "gzip, deflate"));
}
if (contentType != null) {
result.put("content-type", new BasicHeader("Content-Type",
contentType.getMimeType() + "; charset=" +
contentType.getCharset().name()));
}
return result.values().toArray(new Header[result.size()]);
}
private static ContentType getContentType (RequestOptions options) {
ContentType contentType = null;
Map<String, String> headers = options.getHeaders();
if (headers != null) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (entry.getKey().toLowerCase().equals("content-type")) {
String contentTypeValue = entry.getValue();
if (contentTypeValue != null && !contentTypeValue.isEmpty()) {
try {
contentType = ContentType.parse(contentTypeValue);
}
catch (ParseException e) {
throw new HttpClientException(
"Unable to parse request content type", e);
}
catch (UnsupportedCharsetException e) {
throw new HttpClientException(
"Unsupported content type charset", e);
}
if (contentType.getCharset() == null) {
contentType = ContentType.create(
contentType.getMimeType(),
Consts.UTF_8);
}
}
}
}
}
return contentType;
}
private static CoercedRequestOptions coerceRequestOptions(RequestOptions options) {
URI uri = options.getUri();
@ -65,16 +108,27 @@ public class JavaClient {
method = HttpMethod.GET;
}
Header[] headers = prepareHeaders(options);
ContentType contentType = getContentType(options);
Header[] headers = prepareHeaders(options, contentType);
HttpEntity body = null;
if (options.getBody() instanceof String) {
try {
body = new NStringEntity((String)options.getBody());
} catch (UnsupportedEncodingException e) {
throw new HttpClientException("Unable to create request body", e);
String originalBody = (String) options.getBody();
if (contentType != null) {
body = new NStringEntity(originalBody, contentType);
}
else {
try {
body = new NStringEntity(originalBody);
}
catch (UnsupportedEncodingException e) {
throw new HttpClientException(
"Unable to create request body", e);
}
}
} else if (options.getBody() instanceof InputStream) {
body = new InputStreamEntity((InputStream)options.getBody());
}

View file

@ -218,6 +218,8 @@
(defn req-body-app
[req]
{:status 200
:headers (if-let [content-type (:content-type req)]
{"Content-Type" (:content-type req)})
:body (slurp (:body req))})
(tk/defservice test-body-web-service
@ -225,27 +227,98 @@
(init [this context]
(add-ring-handler req-body-app "/hello")
context))
(defn- validate-java-request
[body-to-send headers-to-send expected-content-type expected-response-body]
(let [options (-> (RequestOptions. (URI. "http://localhost:10000/hello/"))
(.setBody body-to-send)
(.setHeaders headers-to-send))
response (SyncHttpClient/post options)]
(is (= 200 (.getStatus response)))
(is (= (-> (.getHeaders response)
(.get "content-type"))
expected-content-type))
(is (= expected-response-body (slurp (.getBody response))))))
(defn- validate-clj-request
[body-to-send headers-to-send expected-content-type expected-response-body]
(let [response (sync/post "http://localhost:10000/hello/"
{:body body-to-send
:headers headers-to-send})]
(is (= 200 (:status response)))
(is (= (get-in response [:headers "content-type"])
expected-content-type))
(is (= expected-response-body (slurp (:body response))))))
(deftest sync-client-request-body-test
(testlogging/with-test-logging
(testutils/with-app-with-config req-body-app
[jetty9/jetty9-service test-body-web-service]
{:webserver {:port 10000}}
(testing "java sync client: string body for post request"
(testing "java sync client: string body for post request with explicit
content type and UTF-8 encoding uses UTF-8 encoding"
(validate-java-request "foo<6F>"
{"Content-Type" "text/plain; charset=utf-8"}
"text/plain; charset=UTF-8"
"foo<6F>"))
(testing "java sync client: string body for post request with explicit
content type and ISO-8859-1 encoding uses ISO-8859-1 encoding"
(validate-java-request "foo<6F>"
{"Content-Type" "text/plain; charset=iso-8859-1"}
"text/plain; charset=ISO-8859-1"
"foo?"))
(testing "java sync client: string body for post request with explicit
content type but without explicit encoding uses UTF-8 encoding"
(validate-java-request "foo<6F>"
{"Content-Type" "text/plain"}
"text/plain; charset=UTF-8"
"foo<6F>"))
(testing "java sync client: string body for post request without explicit
content or encoding uses ISO-8859-1 encoding"
(validate-java-request "foo<6F>"
nil
"text/plain; charset=ISO-8859-1"
"foo?"))
(testing "java sync client: input stream body for post request"
(let [options (-> (RequestOptions. (URI. "http://localhost:10000/hello/"))
(.setBody "foo"))
(.setBody (ByteArrayInputStream.
(.getBytes "foo<6F>" "UTF-8")))
(.setHeaders {"Content-Type"
"text/plain; charset=UTF-8"}))
response (SyncHttpClient/post options)]
(is (= 200 (.getStatus response)))
(is (= "foo" (slurp (.getBody response)))))
(let [options (-> (RequestOptions. (URI. "http://localhost:10000/hello/"))
(.setBody (ByteArrayInputStream. (.getBytes "foo" "UTF-8"))))
response (SyncHttpClient/post options)]
(is (= 200 (.getStatus response)))
(is (= "foo" (slurp (.getBody response))))))
(testing "clojure sync client: string body for post request"
(let [response (sync/post "http://localhost:10000/hello/" {:body (io/input-stream (.getBytes "foo" "UTF-8"))})]
(is (= "foo<6F>" (slurp (.getBody response))))))
(testing "clojure sync client: string body for post request with explicit
content type and UTF-8 encoding uses UTF-8 encoding"
(validate-clj-request "foo<6F>"
{"content-type" "text/plain; charset=utf-8"}
"text/plain; charset=UTF-8"
"foo<6F>"))
(testing "clojure sync client: string body for post request with explicit
content type and ISO-8859 encoding uses ISO-8859-1 encoding"
(validate-clj-request "foo<6F>"
{"content-type" "text/plain; charset=iso-8859-1"}
"text/plain; charset=ISO-8859-1"
"foo?"))
(testing "clojure sync client: string body for post request with explicit
content type but without explicit encoding uses UTF-8 encoding"
(validate-clj-request "foo<6F>"
{"content-type" "text/plain"}
"text/plain; charset=UTF-8"
"foo<6F>"))
(testing "clojure sync client: string body for post request without explicit
content type or encoding uses ISO-8859-1 encoding"
(validate-clj-request "foo<6F>"
{}
"text/plain; charset=ISO-8859-1"
"foo?"))
(testing "clojure sync client: input stream body for post request"
(let [response (sync/post "http://localhost:10000/hello/"
{:body (io/input-stream
(.getBytes "foo<6F>" "UTF-8"))
:headers {"content-type"
"text/plain; charset=UTF-8"}})]
(is (= 200 (:status response)))
(is (= "foo" (slurp (:body response)))))))))
(is (= "foo<EFBFBD>" (slurp (:body response)))))))))
(def compressible-body (apply str (repeat 1000 "f")))