This is an automated email from the ASF dual-hosted git repository. matrei pushed a commit to branch feat/http-client-form-post in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit eb0cc8f1a68ba2abb79ea22fd420f47c7135a0ee Author: Mattias Reichel <[email protected]> AuthorDate: Sun Jun 14 13:26:51 2026 +0200 feat(http-client-support): add httpPostForm method --- .../testing/http/client/HttpClientSupport.groovy | 45 ++++++++++++++++++++++ .../http/client/HttpClientSupportSpec.groovy | 45 ++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/HttpClientSupport.groovy b/grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/HttpClientSupport.groovy index 093940ca7c..2c59a1a6b2 100644 --- a/grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/HttpClientSupport.groovy +++ b/grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/HttpClientSupport.groovy @@ -21,6 +21,7 @@ package org.apache.grails.testing.http.client import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse +import java.lang.reflect.Array import java.time.Duration import groovy.json.JsonGenerator @@ -409,6 +410,25 @@ trait HttpClientSupport { httpPost(headers, pathOrUrl, XmlUtils.toXml(format, body), APPLICATION_XML, client) } + /** + * POST form data as {@code application/x-www-form-urlencoded} (UTF-8). + * + * @param headers optional request headers + * @param pathOrUrl relative path or absolute URL + * @param formData form fields to encode into the request body; collection and array values are encoded as + * repeated keys + * @param client optional {@link HttpClient} instance to use + * @return response with String body + */ + TestHttpResponse httpPostForm( + Map<String, String> headers = EMPTY, + CharSequence pathOrUrl, + Map<String, ?> formData, + HttpClient client = null + ) { + httpPost(headers, pathOrUrl, encodeFormData(formData), 'application/x-www-form-urlencoded', client) + } + // endregion // region POST MULTIPART @@ -1157,6 +1177,31 @@ trait HttpClientSupport { builder.timeout(Duration.ofSeconds(60)) } + private String encodeFormData(Map<String, ?> formData) { + formData.collectMany { + encodeFormValues(it.key, it.value) + }.join('&') + } + + private List<String> encodeFormValues(String key, Object value) { + if (value == null) { + value = '' + } + if (value instanceof Iterable) { + return value.collect { encodeFormEntry(key, it) } + } + if (value.class.array) { + return (0..<Array.getLength(value)).collect { + encodeFormEntry(key, Array.get(value, it)) + } + } + [encodeFormEntry(key, value)] + } + + private String encodeFormEntry(String key, Object value) { + "${URLEncoder.encode(key, 'UTF-8')}=${URLEncoder.encode(value?.toString() ?: '', 'UTF-8')}" + } + private void warn(String[] lines) { println('*** WARNING ***') lines.each { diff --git a/grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/HttpClientSupportSpec.groovy b/grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/HttpClientSupportSpec.groovy index 92afb39b0c..b2794ffe1c 100644 --- a/grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/HttpClientSupportSpec.groovy +++ b/grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/HttpClientSupportSpec.groovy @@ -408,6 +408,51 @@ class HttpClientSupportSpec extends Specification { 'custom' | HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build() } + void 'form request helper encodes multi-value data and forwards headers'() { + given: + server.expectations { + POST('/form/post-default') { + called(1) + decoder('application/x-www-form-urlencoded', Decoders.utf8String) + body('name=Post+Default&tag=red&tag=blue&description=R%26D%2FQA', 'application/x-www-form-urlencoded') + responder { + body('hello') + } + } + POST('/form/post-header-client') { + called(1) + header('X-Req', 'F1') + decoder('application/x-www-form-urlencoded', Decoders.utf8String) + body('name=Post+Header+Client&description=R%26D%2FQA', 'application/x-www-form-urlencoded') + responder { + body('hello') + } + } + } + + and: + def testClient = new TestClient(server.httpUrl) + def customClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build() + def multiValueFormData = [name: 'Post Default', tag: ['red', 'blue'], description: 'R&D/QA'] + + when: + def postResponse = testClient.httpPostForm( + '/form/post-default', + multiValueFormData + ) + def postHeaderResponse = testClient.httpPostForm( + 'X-Req': 'F1', + '/form/post-header-client', + [name: 'Post Header Client', description: 'R&D/QA'], + customClient + ) + + then: + postResponse.assertEquals(200, 'hello') + postHeaderResponse.assertEquals(200, 'hello') + server.verify() + } + @Unroll void 'post with MultipartBody sends multipart content type and payload bytes with #label client'(HttpClient client) { given:
