diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/Request.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/Request.java index 222fbc6d3..4b39dbafc 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/Request.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/Request.java @@ -17,11 +17,13 @@ import org.dromara.hutool.core.lang.Assert; import org.dromara.hutool.core.map.MapUtil; import org.dromara.hutool.core.map.multi.ListValueMap; import org.dromara.hutool.core.net.url.UrlBuilder; +import org.dromara.hutool.core.net.url.UrlQuery; import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.core.util.CharsetUtil; import org.dromara.hutool.core.util.ObjUtil; import org.dromara.hutool.http.GlobalHeaders; import org.dromara.hutool.http.HttpGlobalConfig; +import org.dromara.hutool.http.client.body.FormBody; import org.dromara.hutool.http.client.body.HttpBody; import org.dromara.hutool.http.client.body.StringBody; import org.dromara.hutool.http.client.body.UrlEncodedFormBody; @@ -105,6 +107,10 @@ public class Request implements HeaderOperation { * 最大重定向次数 */ private int maxRedirectCount; + /** + * 是否是REST请求模式,REST模式运行GET请求附带body + */ + private boolean isRest; /** * 默认构造 @@ -148,6 +154,15 @@ public class Request implements HeaderOperation { return url; } + /** + * 获取处理后的请求URL,即如果为非REST的GET请求,将form类型的body拼接为URL的一部分 + * + * @return URL + */ + public UrlBuilder handledUrl() { + return urlWithParamIfGet(); + } + /** * 设置URL * @@ -227,12 +242,24 @@ public class Request implements HeaderOperation { /** * 获取请求体 * - * @return this + * @return 请求体 */ public HttpBody body() { return this.body; } + /** + * 获取处理过的请求体,即如果是非REST的GET请求,始终返回{@code null} + * + * @return 请求体 + */ + public HttpBody handledBody() { + if (Method.GET.equals(method) && !this.isRest) { + return null; + } + return body(); + } + /** * 添加请求表单内容 * @@ -291,6 +318,18 @@ public class Request implements HeaderOperation { return this; } + /** + * 设置是否rest模式
+ * rest模式下get请求不会把参数附加到URL之后,而是作为body发送 + * + * @param isRest 是否rest模式 + * @return this + */ + public Request setRest(final boolean isRest) { + this.isRest = isRest; + return this; + } + /** * 发送请求 * @@ -306,7 +345,30 @@ public class Request implements HeaderOperation { * @param engine 自自定义引擎 * @return 响应内容 */ - public Response send(final ClientEngine engine){ + public Response send(final ClientEngine engine) { return engine.send(this); } + + /** + * 对于GET请求将参数加到URL中
+ * 此处不对URL中的特殊字符做单独编码
+ * 对于非rest的GET请求,且处于重定向时,参数丢弃 + */ + private UrlBuilder urlWithParamIfGet() { + if (Method.GET.equals(method) && !this.isRest) { + final HttpBody body = this.body; + if (body instanceof FormBody) { + final UrlBuilder urlBuilder = UrlBuilder.of(this.url.toURL(), this.url.getCharset()); + UrlQuery query = urlBuilder.getQuery(); + if (null == query) { + query = UrlQuery.of(); + urlBuilder.setQuery(query); + } + query.addAll(((FormBody) body).form()); + return urlBuilder; + } + } + + return this.url(); + } } diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/FormBody.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/FormBody.java index 37de27cd6..320ab337b 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/FormBody.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/FormBody.java @@ -55,6 +55,15 @@ public abstract class FormBody> implements HttpBody { this.charset = charset; } + /** + * 获取表单内容 + * + * @return 表单内容 + */ + public Map form(){ + return this.form; + } + /** * 设置map类型表单数据 * diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Engine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Engine.java index a289f2b8f..2c8d9b358 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Engine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Engine.java @@ -141,7 +141,7 @@ public class HttpClient4Engine implements ClientEngine { * @return {@link HttpUriRequest} */ private static HttpUriRequest buildRequest(final Request message) { - final UrlBuilder url = message.url(); + final UrlBuilder url = message.handledUrl(); Assert.notNull(url, "Request URL must be not null!"); final URI uri = url.toURI(); @@ -153,7 +153,7 @@ public class HttpClient4Engine implements ClientEngine { message.headers().forEach((k, v1) -> v1.forEach((v2) -> requestBuilder.addHeader(k, v2))); // 填充自定义消息体 - final HttpBody body = message.body(); + final HttpBody body = message.handledBody(); if(null != body){ requestBuilder.setEntity(new HttpClient4BodyEntity( // 用户自定义的内容类型 diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Engine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Engine.java index 51223f0f7..d7af84e68 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Engine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Engine.java @@ -31,15 +31,14 @@ import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.message.BasicHeader; import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.lang.Assert; -import org.dromara.hutool.core.lang.Console; import org.dromara.hutool.core.net.url.UrlBuilder; import org.dromara.hutool.http.GlobalHeaders; import org.dromara.hutool.http.HttpException; import org.dromara.hutool.http.client.ClientConfig; -import org.dromara.hutool.http.client.engine.ClientEngine; import org.dromara.hutool.http.client.Request; import org.dromara.hutool.http.client.Response; import org.dromara.hutool.http.client.body.HttpBody; +import org.dromara.hutool.http.client.engine.ClientEngine; import org.dromara.hutool.http.meta.HeaderName; import org.dromara.hutool.http.proxy.HttpProxy; import org.dromara.hutool.http.ssl.SSLInfo; @@ -143,7 +142,7 @@ public class HttpClient5Engine implements ClientEngine { */ @SuppressWarnings("ConstantConditions") private static ClassicHttpRequest buildRequest(final Request message) { - final UrlBuilder url = message.url(); + final UrlBuilder url = message.handledUrl(); Assert.notNull(url, "Request URL must be not null!"); final URI uri = url.toURI(); @@ -153,7 +152,7 @@ public class HttpClient5Engine implements ClientEngine { request.setHeaders(toHeaderList(message.headers()).toArray(new Header[0])); // 填充自定义消息体 - final HttpBody body = message.body(); + final HttpBody body = message.handledBody(); if(null != body){ request.setEntity(new HttpClient5BodyEntity( // 用户自定义的内容类型 diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java index 8ce58689b..6857482a6 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java @@ -100,7 +100,7 @@ public class JdkClientEngine implements ClientEngine { * @throws IOException IO异常 */ private void doSend(final JdkHttpConnection conn, final Request message) throws IOException { - final HttpBody body = message.body(); + final HttpBody body = message.handledBody(); if (null != body) { // 带有消息体,一律按照Rest方式发送 body.writeClose(conn.getOutputStream()); @@ -121,7 +121,7 @@ public class JdkClientEngine implements ClientEngine { final ClientConfig config = ObjUtil.defaultIfNull(this.config, ClientConfig::of); final JdkHttpConnection conn = JdkHttpConnection - .of(message.url().toURL(), config.getProxy()) + .of(message.handledUrl().toURL(), config.getProxy()) .setConnectTimeout(config.getConnectionTimeout()) .setReadTimeout(config.getReadTimeout()) .setMethod(message.method())// @@ -162,7 +162,7 @@ public class JdkClientEngine implements ClientEngine { if (code != HttpURLConnection.HTTP_OK) { if (HttpStatus.isRedirected(code)) { - message.url(getLocationUrl(message.url(), conn.header(HeaderName.LOCATION))); + message.url(getLocationUrl(message.handledUrl(), conn.header(HeaderName.LOCATION))); if (redirectCount < message.maxRedirectCount()) { redirectCount++; return send(message, isAsync); diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkHttpConnection.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkHttpConnection.java index 4766b1280..f1a9ec838 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkHttpConnection.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkHttpConnection.java @@ -105,9 +105,9 @@ public class JdkHttpConnection implements HeaderOperation, Cl */ public JdkHttpConnection setMethod(final Method method) { if (Method.POST.equals(method) // - || Method.PUT.equals(method)// - || Method.PATCH.equals(method)// - || Method.DELETE.equals(method)) { + || Method.PUT.equals(method)// + || Method.PATCH.equals(method)// + || Method.DELETE.equals(method)) { this.conn.setUseCaches(false); // 增加PATCH方法支持 @@ -364,7 +364,11 @@ public class JdkHttpConnection implements HeaderOperation, Cl // 修改为POST,而且无法调用setRequestMethod方法修改,因此此处使用反射强制修改字段属性值 // https://stackoverflow.com/questions/978061/http-get-with-request-body/983458 if (method == Method.GET && method != getMethod()) { - FieldUtil.setFieldValue(this.conn, "method", Method.GET.name()); + try { + FieldUtil.setFieldValue(this.conn, "method", Method.GET.name()); + } catch (final RuntimeException ignore) { + // JDK9+中可能反射失败,忽略之 + } } return out; @@ -410,7 +414,6 @@ public class JdkHttpConnection implements HeaderOperation, Cl /** * 断开连接 - * */ @Override public void close() { diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java index b9cf0b0db..afac4f617 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java @@ -13,13 +13,12 @@ package org.dromara.hutool.http.client.engine.okhttp; import okhttp3.OkHttpClient; -import okhttp3.internal.http.HttpMethod; import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.http.client.ClientConfig; -import org.dromara.hutool.http.client.body.HttpBody; -import org.dromara.hutool.http.client.engine.ClientEngine; import org.dromara.hutool.http.client.Request; import org.dromara.hutool.http.client.Response; +import org.dromara.hutool.http.client.body.HttpBody; +import org.dromara.hutool.http.client.engine.ClientEngine; import org.dromara.hutool.http.proxy.HttpProxy; import org.dromara.hutool.http.ssl.SSLInfo; @@ -126,14 +125,14 @@ public class OkHttpEngine implements ClientEngine { */ private static okhttp3.Request buildRequest(final Request message) { final okhttp3.Request.Builder builder = new okhttp3.Request.Builder() - .url(message.url().toURL()); + .url(message.handledUrl().toURL()); // 填充方法 final String method = message.method().name(); - final HttpBody body = message.body(); + final HttpBody body = message.handledBody(); // if (HttpMethod.permitsRequestBody(method)) { if (null != body) { - // 为了兼容支持rest请求,在此不区分是否为GET等方法,一律按照body是否有值填充 + // 为了兼容支持rest请求,在此不区分是否为GET等方法,一律按照body是否有值填充,兼容 builder.method(method, new OkHttpRequestBody(body)); } else { builder.method(method, null); diff --git a/hutool-http/src/test/java/org/dromara/hutool/http/IssueI85C9STest.java b/hutool-http/src/test/java/org/dromara/hutool/http/IssueI85C9STest.java new file mode 100644 index 000000000..d94aec26c --- /dev/null +++ b/hutool-http/src/test/java/org/dromara/hutool/http/IssueI85C9STest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023. looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.http; + +import org.dromara.hutool.core.lang.Console; +import org.dromara.hutool.core.map.MapBuilder; +import org.dromara.hutool.http.client.Request; +import org.dromara.hutool.http.client.Response; +import org.dromara.hutool.http.client.engine.httpclient4.HttpClient4Engine; +import org.dromara.hutool.http.meta.Method; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; + +public class IssueI85C9STest { + @Test + void getWithFormTest() { + final Response send = Request.of("http://localhost:8888/formTest") + .method(Method.GET) + .form(MapBuilder.of(new HashMap()).put("a", 1).put("b", 2).build()) + .send(new HttpClient4Engine()); + + Console.log(send.bodyStr()); + } +} diff --git a/hutool-http/src/test/java/org/dromara/hutool/http/server/SimpleServerTest.java b/hutool-http/src/test/java/org/dromara/hutool/http/server/SimpleServerTest.java index 4bd1beecf..6975fcb6d 100644 --- a/hutool-http/src/test/java/org/dromara/hutool/http/server/SimpleServerTest.java +++ b/hutool-http/src/test/java/org/dromara/hutool/http/server/SimpleServerTest.java @@ -27,56 +27,58 @@ public class SimpleServerTest { public static void main(final String[] args) { HttpUtil.createServer(8888) - .addFilter(((req, res, chain) -> { - Console.log("Filter: " + req.getPath()); - chain.doFilter(req.getHttpExchange()); - })) - // 设置默认根目录,classpath/html - .setRoot(FileUtil.file("html")) - // get数据测试,返回请求的PATH - .addAction("/get", (request, response) -> - response.write(request.getURI().toString(), ContentType.TEXT_PLAIN.toString()) - ) - // 返回JSON数据测试 - .addAction("/restTest", (request, response) -> { - final String res = JSONUtil.ofObj() - .set("id", 1) - .set("method", request.getMethod()) - .set("request", request.getBody()) - .toStringPretty(); - response.write(res, ContentType.JSON.toString()); - }) - // 获取表单数据测试 - // http://localhost:8888/formTest?a=1&a=2&b=3 - .addAction("/formTest", (request, response) -> - response.write(request.getParams().toString(), ContentType.TEXT_PLAIN.toString()) - ) + .addFilter(((req, res, chain) -> { + Console.log("Filter: " + req.getPath()); + chain.doFilter(req.getHttpExchange()); + })) + // 设置默认根目录,classpath/html + .setRoot(FileUtil.file("html")) + // get数据测试,返回请求的PATH + .addAction("/get", (request, response) -> + response.write(request.getURI().toString(), ContentType.TEXT_PLAIN.toString()) + ) + // 返回JSON数据测试 + .addAction("/restTest", (request, response) -> { + final String res = JSONUtil.ofObj() + .set("id", 1) + .set("method", request.getMethod()) + .set("request", request.getBody()) + .toStringPretty(); + response.write(res, ContentType.JSON.toString()); + }) + // 获取表单数据测试 + // http://localhost:8888/formTest?a=1&a=2&b=3 + .addAction("/formTest", (request, response) -> { + Console.log(request.getMethod()); + response.write(request.getParams().toString(), ContentType.TEXT_PLAIN.toString()); + } + ) - // 文件上传测试 - // http://localhost:8888/formForUpload.html - .addAction("/file", (request, response) -> { - Console.log("Upload file..."); - Console.log(request.getParams()); - final UploadFile[] files = request.getMultipart().getFiles("file"); - // 传入目录,默认读取HTTP头中的文件名然后创建文件 - for (final UploadFile file : files) { - file.write("d:/test/"); - Console.log("Write file: d:/test/" + file.getFileName()); - } - response.write(request.getMultipart().getParamMap().toString(), ContentType.TEXT_PLAIN.toString()); - } - ) - // 测试输出响应内容是否能正常返回Content-Length头信息 - .addAction("test/zeroStr", (req, res)-> { - res.write("0"); - Console.log("Write 0 OK"); - }).addAction("/getCookie", ((request, response) -> { - response.setHeader(HeaderName.SET_COOKIE.toString(), - ListUtil.of( - new HttpCookie("cc", "123").toString(), - new HttpCookie("cc", "abc").toString())); - response.write("Cookie ok"); - })) - .start(); + // 文件上传测试 + // http://localhost:8888/formForUpload.html + .addAction("/file", (request, response) -> { + Console.log("Upload file..."); + Console.log(request.getParams()); + final UploadFile[] files = request.getMultipart().getFiles("file"); + // 传入目录,默认读取HTTP头中的文件名然后创建文件 + for (final UploadFile file : files) { + file.write("d:/test/"); + Console.log("Write file: d:/test/" + file.getFileName()); + } + response.write(request.getMultipart().getParamMap().toString(), ContentType.TEXT_PLAIN.toString()); + } + ) + // 测试输出响应内容是否能正常返回Content-Length头信息 + .addAction("test/zeroStr", (req, res) -> { + res.write("0"); + Console.log("Write 0 OK"); + }).addAction("/getCookie", ((request, response) -> { + response.setHeader(HeaderName.SET_COOKIE.toString(), + ListUtil.of( + new HttpCookie("cc", "123").toString(), + new HttpCookie("cc", "abc").toString())); + response.write("Cookie ok"); + })) + .start(); } }