(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:
parent
611218fb23
commit
f83d7821ad
3 changed files with 178 additions and 24 deletions
|
@ -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)}))
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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")))
|
||||
|
||||
|
|
Loading…
Reference in a new issue