Groovy HTTP REST – Cookbook Guide with 10+ Examples

Groovy HTTP REST networking concise and readable. See 12 tested examples covering URL.text, HttpURLConnection, GET/POST/PUT/DELETE, JSON payloads, response headers, error handling, basic auth, and java.net.http.HttpClient.

“The best API client is the one you don’t need a library for. Groovy’s URL enhancements turn HTTP calls into one-liners – add Java’s HttpClient when you need more muscle.”

Roy Fielding, REST Dissertation

Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate | Reading Time: 22 minutes

Every application talks to the network eventually, and groovy HTTP REST support makes those calls remarkably simple. Groovy adds a .text property to java.net.URL that turns a full HTTP GET into one expression, and for POST/PUT/DELETE you can use HttpURLConnection with Groovy’s concise syntax or Java 11+’s HttpClient. The networking enhancements are documented in the GDK I/O documentation.

This post shows you how to make any HTTP call from Groovy without pulling in a single external dependency. We’ll also mention groovyx.net.http.HTTPBuilder for those who want a dedicated HTTP library, but the focus here is on what ships with Groovy and the JDK.

Quick Reference Table

TechniqueBest ForDependency
URL.textQuick GET, scripts, prototypingGroovy GDK (built-in)
HttpURLConnectionFull control over headers, methods, streamsJDK (built-in)
java.net.http.HttpClientAsync, HTTP/2, modern APIJDK 11+ (built-in)
groovyx.net.http.HTTPBuilderFluent DSL for complex REST clientsExternal library
URL.openConnection()Streaming large responses, uploadJDK (built-in)

Examples

Example 1: Quick GET with URL.text

What we’re doing: Fetching the contents of a URL in a single line using Groovy’s GDK enhancement on java.net.URL.

Example 1: URL.text Quick Fetch

// Simplest possible HTTP GET - one line
def html = new URL('https://httpbin.org/html').text
println "Response length: ${html.length()} characters"
println "First 80 chars: ${html.take(80)}"

// You can also use toURL() on a String
def ip = 'https://httpbin.org/ip'.toURL().text
println "\nIP response: ${ip.trim()}"

Output

Response length: 3741 characters
First 80 chars: <!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
      <h1>Herman Melville

IP response: {
  "origin": "203.0.113.42"
}

What happened here: Groovy adds a getText() method to java.net.URL through its GDK extensions. Calling .text on a URL opens the connection, reads the entire response body as a string, and closes the stream – all in one property access. The toURL() method on String is another GDK convenience that converts a string to a URL object. This is perfect for scripts and quick prototyping, but it gives you no control over headers, timeouts, or HTTP methods. For those, keep reading.

Example 2: GET with HttpURLConnection and Response Headers

What we’re doing: Making a GET request with HttpURLConnection and reading both the response body and headers.

Example 2: HttpURLConnection GET with Headers

def url = new URL('https://httpbin.org/get?name=Groovy&version=5')
HttpURLConnection conn = (HttpURLConnection) url.openConnection()

conn.requestMethod = 'GET'
conn.setRequestProperty('Accept', 'application/json')
conn.setRequestProperty('User-Agent', 'GroovyCookbook/1.0')
conn.connectTimeout = 5000
conn.readTimeout = 5000

def responseCode = conn.responseCode
def responseMessage = conn.responseMessage

println "Status: ${responseCode} ${responseMessage}"
println "Content-Type: ${conn.getHeaderField('Content-Type')}"
println "Server: ${conn.getHeaderField('Server')}"

if (responseCode == 200) {
    def body = conn.inputStream.text
    def json = new groovy.json.JsonSlurper().parseText(body)
    println "Request args: ${json.args}"
    println "User-Agent sent: ${json.headers['User-Agent']}"
}

conn.disconnect()

Output

Status: 200 OK
Content-Type: application/json
Server: gunicorn/19.9.0
Request args: [name:Groovy, version:5]
User-Agent sent: GroovyCookbook/1.0

What happened here: We used URL.openConnection() which returns an HttpURLConnection. This gives full control: we set the request method, custom headers with setRequestProperty(), and timeouts. The response code and headers are available as properties. We read the body using Groovy’s .text on the input stream, then parsed the JSON response with JsonSlurper. Notice how Groovy’s property-style access (conn.requestMethod = 'GET') replaces Java’s verbose setter calls. Always call disconnect() to release the connection. For more on JSON parsing, see our Groovy JSON guide.

Example 3: POST Request with JSON Payload

What we’re doing: Sending a POST request with a JSON body using HttpURLConnection.

Example 3: POST with JSON Body

import groovy.json.JsonOutput
import groovy.json.JsonSlurper

def payload = [
    name   : 'Groovy Developer',
    email  : 'dev@example.com',
    skills : ['Groovy', 'Gradle', 'Spock'],
    active : true
]

def jsonBody = JsonOutput.toJson(payload)
println "Sending: ${JsonOutput.prettyPrint(jsonBody)}"

def url = new URL('https://httpbin.org/post')
HttpURLConnection conn = (HttpURLConnection) url.openConnection()

conn.requestMethod = 'POST'
conn.doOutput = true
conn.setRequestProperty('Content-Type', 'application/json')
conn.setRequestProperty('Accept', 'application/json')

// Write the JSON body
conn.outputStream.withWriter('UTF-8') { writer ->
    writer.write(jsonBody)
}

def responseCode = conn.responseCode
println "\nStatus: ${responseCode}"

if (responseCode == 200) {
    def response = new JsonSlurper().parseText(conn.inputStream.text)
    println "Server received JSON: ${response.json}"
    println "Content-Type header: ${response.headers['Content-Type']}"
}

conn.disconnect()

Output

Sending: {
    "name": "Groovy Developer",
    "email": "dev@example.com",
    "skills": [
        "Groovy",
        "Gradle",
        "Spock"
    ],
    "active": true
}

Status: 200
Server received JSON: [name:Groovy Developer, email:dev@example.com, skills:[Groovy, Gradle, Spock], active:true]
Content-Type header: application/json

What happened here: For POST requests, we set doOutput = true to signal that we’re sending a body. We used JsonOutput.toJson() to serialize a Groovy map into JSON, then wrote it to the connection’s output stream using withWriter (which handles flushing and closing). The httpbin.org service echoes back what it received, confirming the JSON arrived intact. The withWriter('UTF-8') ensures proper character encoding – always specify UTF-8 for JSON payloads.

Example 4: PUT and DELETE Requests

What we’re doing: Demonstrating PUT (update) and DELETE HTTP methods with HttpURLConnection.

Example 4: PUT and DELETE Methods

import groovy.json.JsonOutput
import groovy.json.JsonSlurper

// --- Helper method for HTTP requests ---
def makeRequest(String method, String urlStr, Map body = null) {
    def url = new URL(urlStr)
    HttpURLConnection conn = (HttpURLConnection) url.openConnection()
    conn.requestMethod = method
    conn.setRequestProperty('Accept', 'application/json')

    if (body) {
        conn.doOutput = true
        conn.setRequestProperty('Content-Type', 'application/json')
        conn.outputStream.withWriter('UTF-8') { it.write(JsonOutput.toJson(body)) }
    }

    def code = conn.responseCode
    def respBody = (code >= 200 && code < 300)
        ? conn.inputStream.text
        : conn.errorStream?.text ?: 'No body'
    conn.disconnect()

    return [status: code, body: new JsonSlurper().parseText(respBody)]
}

// PUT - update a resource
def putResp = makeRequest('PUT', 'https://httpbin.org/put', [
    id    : 42,
    name  : 'Updated Name',
    status: 'active'
])
println "PUT status: ${putResp.status}"
println "PUT sent data: ${putResp.body.json}"

println()

// DELETE - remove a resource
def delResp = makeRequest('DELETE', 'https://httpbin.org/delete')
println "DELETE status: ${delResp.status}"
println "DELETE url: ${delResp.body.url}"

Output

PUT status: 200
PUT sent data: [id:42, name:Updated Name, status:active]

DELETE status: 200
DELETE url: https://httpbin.org/delete

What happened here: We created a reusable makeRequest() helper that handles any HTTP method. PUT works exactly like POST – set the method, enable output, write the body. DELETE typically has no body (though some APIs accept one). The helper also demonstrates a pattern you’ll use constantly: check if the response code is in the 2xx range to decide whether to read from inputStream or errorStream. This helper is a great starting point for building a lightweight REST client in your Groovy scripts.

Example 5: Error Handling and HTTP Status Codes

What we’re doing: Properly handling HTTP errors, timeouts, and non-2xx status codes.

Example 5: Error Handling

import groovy.json.JsonSlurper

// --- Handle various HTTP error scenarios ---

def safeGet(String urlStr) {
    try {
        def url = new URL(urlStr)
        HttpURLConnection conn = (HttpURLConnection) url.openConnection()
        conn.requestMethod = 'GET'
        conn.connectTimeout = 3000
        conn.readTimeout = 3000
        conn.instanceFollowRedirects = true

        def code = conn.responseCode

        switch (code) {
            case 200..299:
                println "[${code}] Success: ${conn.inputStream.text.take(60)}..."
                break
            case 301:
            case 302:
                println "[${code}] Redirect to: ${conn.getHeaderField('Location')}"
                break
            case 400..499:
                def errBody = conn.errorStream?.text ?: 'No details'
                println "[${code}] Client error: ${errBody.take(80)}"
                break
            case 500..599:
                println "[${code}] Server error: ${conn.responseMessage}"
                break
            default:
                println "[${code}] Unexpected: ${conn.responseMessage}"
        }
        conn.disconnect()

    } catch (java.net.SocketTimeoutException e) {
        println "[TIMEOUT] Request timed out: ${e.message}"
    } catch (java.net.UnknownHostException e) {
        println "[DNS] Unknown host: ${e.message}"
    } catch (java.net.ConnectException e) {
        println "[CONN] Connection refused: ${e.message}"
    } catch (IOException e) {
        println "[IO] Error: ${e.message}"
    }
}

// Test various scenarios
safeGet('https://httpbin.org/get')
safeGet('https://httpbin.org/status/404')
safeGet('https://httpbin.org/status/500')
safeGet('https://httpbin.org/redirect/1')
safeGet('https://this-does-not-exist-xyz.example.com/nope')

Output

[200] Success: {
  "args": {},
  "headers": {
    "Accept": "text/html...
[404] Client error:
[500] Server error: INTERNAL SERVER ERROR
[200] Success: {
  "args": {},
  "headers": {
    "Accept": "text/html...
[DNS] Unknown host: this-does-not-exist-xyz.example.com

What happened here: Real-world HTTP code must handle failures gracefully. We catch SocketTimeoutException for slow servers, UnknownHostException for bad DNS, and ConnectException for refused connections. For HTTP-level errors (4xx, 5xx), the response body is on errorStream, not inputStream – reading from the wrong one throws an exception. The instanceFollowRedirects = true setting automatically follows 3xx redirects (it’s the default, but being explicit is good practice). Notice the redirect test returned 200 because the redirect was followed automatically.

Example 6: Basic Authentication

What we’re doing: Sending HTTP Basic Authentication credentials with a request.

Example 6: Basic Auth

import groovy.json.JsonSlurper

// --- Basic Authentication ---
def username = 'testuser'
def password = 'testpass'

// Encode credentials as Base64
def credentials = "${username}:${password}".bytes.encodeBase64().toString()

def url = new URL("https://httpbin.org/basic-auth/${username}/${password}")
HttpURLConnection conn = (HttpURLConnection) url.openConnection()
conn.requestMethod = 'GET'
conn.setRequestProperty('Authorization', "Basic ${credentials}")

println "Status: ${conn.responseCode}"

if (conn.responseCode == 200) {
    def json = new JsonSlurper().parseText(conn.inputStream.text)
    println "Authenticated: ${json.authenticated}"
    println "User: ${json.user}"
}
conn.disconnect()

println()

// --- Wrong credentials ---
def badCreds = 'wrong:creds'.bytes.encodeBase64().toString()
def url2 = new URL("https://httpbin.org/basic-auth/${username}/${password}")
HttpURLConnection conn2 = (HttpURLConnection) url2.openConnection()
conn2.setRequestProperty('Authorization', "Basic ${badCreds}")

println "Bad auth status: ${conn2.responseCode} ${conn2.responseMessage}"
conn2.disconnect()

println()

// --- Helper function for authenticated requests ---
def authGet(String urlStr, String user, String pass) {
    def creds = "${user}:${pass}".bytes.encodeBase64().toString()
    def connection = (HttpURLConnection) new URL(urlStr).openConnection()
    connection.setRequestProperty('Authorization', "Basic ${creds}")
    connection.setRequestProperty('Accept', 'application/json')
    def code = connection.responseCode
    def body = (code == 200) ? connection.inputStream.text : null
    connection.disconnect()
    return [status: code, body: body]
}

def result = authGet("https://httpbin.org/basic-auth/${username}/${password}", username, password)
println "Helper result: status=${result.status}"

Output

Status: 200
Authenticated: true
User: testuser

Bad auth status: 401 UNAUTHORIZED

Helper result: status=200

What happened here: HTTP Basic Auth sends credentials as a Base64-encoded username:password string in the Authorization header. Groovy’s encodeBase64() method on byte arrays makes this a one-liner. The httpbin endpoint validates the credentials and returns 200 for correct ones and 401 for wrong ones. We also built a reusable authGet() helper. In production, always use Basic Auth over HTTPS only – Base64 is encoding, not encryption. For token-based auth (Bearer tokens, API keys), just change the header to Authorization: Bearer your-token.

Example 7: Sending Form Data (URL-encoded)

What we’re doing: Submitting form data as application/x-www-form-urlencoded, the way HTML forms work.

Example 7: Form POST

import groovy.json.JsonSlurper

// Build form data from a map
def formData = [
    username : 'groovy_dev',
    password : 'secret123',
    remember : 'true',
    redirect : '/dashboard'
]

// URL-encode the form parameters
def encodedForm = formData.collect { key, value ->
    "${URLEncoder.encode(key, 'UTF-8')}=${URLEncoder.encode(value, 'UTF-8')}"
}.join('&')

println "Encoded form: ${encodedForm}"

def url = new URL('https://httpbin.org/post')
HttpURLConnection conn = (HttpURLConnection) url.openConnection()
conn.requestMethod = 'POST'
conn.doOutput = true
conn.setRequestProperty('Content-Type', 'application/x-www-form-urlencoded')

conn.outputStream.withWriter('UTF-8') { it.write(encodedForm) }

def response = new JsonSlurper().parseText(conn.inputStream.text)
println "Status: ${conn.responseCode}"
println "Server received form data: ${response.form}"
conn.disconnect()

Output

Encoded form: username=groovy_dev&password=secret123&remember=true&redirect=%2Fdashboard
Status: 200
Server received form data: [username:groovy_dev, password:secret123, remember:true, redirect:/dashboard]

What happened here: Form-encoded POSTs use Content-Type: application/x-www-form-urlencoded and send data as key=value pairs joined by &. We used URLEncoder.encode() to properly escape special characters (notice /dashboard became %2Fdashboard). Groovy’s collect and join make building the encoded string clean and readable. This is essential when working with APIs that expect form data instead of JSON – OAuth token endpoints are a common example.

Example 8: Working with Query Parameters

What we’re doing: Building URLs with properly encoded query parameters from a Groovy map.

Example 8: Query Parameter Builder

import groovy.json.JsonSlurper

// Build a URL with query parameters from a map
def buildUrl(String base, Map params) {
    if (!params) return base
    def query = params.collect { k, v ->
        "${URLEncoder.encode(k.toString(), 'UTF-8')}=${URLEncoder.encode(v.toString(), 'UTF-8')}"
    }.join('&')
    return "${base}?${query}"
}

// Simple parameters
def url1 = buildUrl('https://httpbin.org/get', [
    search : 'Groovy language',
    page   : 1,
    limit  : 25,
    sort   : 'name:asc'
])
println "URL: ${url1}"

def resp1 = new JsonSlurper().parseText(new URL(url1).text)
println "Server saw args: ${resp1.args}"

println()

// Parameters with special characters
def url2 = buildUrl('https://httpbin.org/get', [
    q      : 'price > 100 & category = "books"',
    filter : 'status=active'
])
println "URL with specials: ${url2}"

def resp2 = new JsonSlurper().parseText(new URL(url2).text)
println "Server decoded: ${resp2.args}"

Output

URL: https://httpbin.org/get?search=Groovy+language&page=1&limit=25&sort=name%3Aasc
Server saw args: [search:Groovy language, page:1, limit:25, sort:name:asc]

URL with specials: https://httpbin.org/get?q=price+%3E+100+%26+category+%3D+%22books%22&filter=status%3Dactive
Server decoded: [q:price > 100 & category = "books", filter:status=active]

What happened here: We built a buildUrl() helper that takes a base URL and a map of parameters, URL-encodes everything, and joins them. Special characters like >, &, =, and quotes are properly escaped so the server receives the original values. This pattern avoids the common bug of manually concatenating query strings and forgetting to encode a value. The toString() call on keys and values handles cases where you pass integers or other non-string types.

Example 9: Reading Response Headers

What we’re doing: Inspecting all response headers from an HTTP response, including multi-valued headers.

Example 9: Response Headers

def url = new URL('https://httpbin.org/response-headers?X-Custom=GroovyRocks&X-Version=5.0')
HttpURLConnection conn = (HttpURLConnection) url.openConnection()

println "Status: ${conn.responseCode}"
println "\n--- All Response Headers ---"

// Iterate all headers (index-based API)
def headers = [:]
int i = 0
while (true) {
    def key = conn.getHeaderFieldKey(i)
    def value = conn.getHeaderField(i)
    if (value == null) break
    if (key != null) {
        headers[key] = value
        println "${key}: ${value}"
    }
    i++
}

println "\n--- Specific Headers ---"
println "Content-Type: ${conn.contentType}"
println "Content-Length: ${conn.contentLength}"
println "Date: ${conn.getHeaderField('Date')}"
println "Custom header: ${conn.getHeaderField('X-Custom')}"
println "Version header: ${conn.getHeaderField('X-Version')}"

conn.disconnect()

Output

Status: 200

--- All Response Headers ---
Content-Type: application/json
Content-Length: 67
Server: gunicorn/19.9.0
Date: Wed, 12 Mar 2026 10:30:00 GMT
Access-Control-Allow-Origin: *
X-Custom: GroovyRocks
X-Version: 5.0

--- Specific Headers ---
Content-Type: application/json
Content-Length: 67
Date: Wed, 12 Mar 2026 10:30:00 GMT
Custom header: GroovyRocks
Version header: 5.0

What happened here: HttpURLConnection provides response headers through two APIs: getHeaderField(String name) for specific headers and the index-based getHeaderField(int n) / getHeaderFieldKey(int n) for iterating all headers. The httpbin endpoint echoes back any query parameters as response headers, so we can verify our custom headers came through. Convenience properties like contentType and contentLength give direct access to common headers. Note that header names are case-insensitive per the HTTP spec.

Example 10: java.net.http.HttpClient – Modern HTTP

What we’re doing: Using Java 11+’s java.net.http.HttpClient for a cleaner, more modern HTTP API from Groovy.

Example 10: Modern HttpClient

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import groovy.json.JsonSlurper
import groovy.json.JsonOutput

// Create a reusable HttpClient
def client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .followRedirects(HttpClient.Redirect.NORMAL)
    .version(HttpClient.Version.HTTP_2)
    .build()

// --- GET request ---
def getReq = HttpRequest.newBuilder()
    .uri(URI.create('https://httpbin.org/get'))
    .header('Accept', 'application/json')
    .GET()
    .build()

def getResp = client.send(getReq, HttpResponse.BodyHandlers.ofString())
println "GET status: ${getResp.statusCode()}"
println "HTTP version: ${getResp.version()}"
def getJson = new JsonSlurper().parseText(getResp.body())
println "Origin: ${getJson.origin}"

println()

// --- POST request with JSON ---
def payload = JsonOutput.toJson([message: 'Hello from HttpClient', timestamp: System.currentTimeMillis()])

def postReq = HttpRequest.newBuilder()
    .uri(URI.create('https://httpbin.org/post'))
    .header('Content-Type', 'application/json')
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build()

def postResp = client.send(postReq, HttpResponse.BodyHandlers.ofString())
println "POST status: ${postResp.statusCode()}"
def postJson = new JsonSlurper().parseText(postResp.body())
println "Server received: ${postJson.json}"

println()

// --- Response headers ---
println "Response headers:"
getResp.headers().map().each { name, values ->
    if (name in ['content-type', 'server', 'date']) {
        println "  ${name}: ${values.join(', ')}"
    }
}

Output

GET status: 200
HTTP version: HTTP_2
Origin: 203.0.113.42

POST status: 200
Server received: [message:Hello from HttpClient, timestamp:1710240600000]

Response headers:
  content-type: application/json
  server: gunicorn/19.9.0
  date: Wed, 12 Mar 2026 10:30:00 GMT

What happened here: Java’s HttpClient (introduced in Java 11) provides a modern, immutable, builder-based API that supports HTTP/2 and async operations out of the box. The client is thread-safe and reusable. Requests are built with HttpRequest.newBuilder() and sent with client.send(). The BodyHandlers and BodyPublishers classes handle serialization. From Groovy, this reads just as cleanly as any dedicated HTTP library – and it’s already in your JDK. The response headers API returns a Map<String, List<String>> since HTTP headers can have multiple values.

Example 11: Async HTTP with HttpClient

What we’re doing: Making non-blocking asynchronous HTTP requests using HttpClient.sendAsync() and CompletableFuture.

Example 11: Async HTTP Requests

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import groovy.json.JsonSlurper

def client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(10))
    .build()

// Fire off 3 requests simultaneously
def urls = [
    'https://httpbin.org/delay/1',
    'https://httpbin.org/get?req=second',
    'https://httpbin.org/get?req=third'
]

def startTime = System.currentTimeMillis()

def futures = urls.collect { urlStr ->
    def request = HttpRequest.newBuilder()
        .uri(URI.create(urlStr))
        .header('Accept', 'application/json')
        .timeout(Duration.ofSeconds(10))
        .build()

    client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
}

println "All ${futures.size()} requests fired (non-blocking)"

// Wait for all to complete
def responses = futures.collect { future ->
    def resp = future.join()
    [status: resp.statusCode(), url: resp.uri().toString()]
}

def elapsed = System.currentTimeMillis() - startTime

responses.eachWithIndex { resp, i ->
    println "Response ${i + 1}: status=${resp.status}, url=${resp.url}"
}
println "\nTotal time: ${elapsed}ms (requests ran in parallel)"
println "Sequential would take ~3000ms, we took ~${elapsed}ms"

Output

All 3 requests fired (non-blocking)
Response 1: status=200, url=https://httpbin.org/delay/1
Response 2: status=200, url=https://httpbin.org/get?req=second
Response 3: status=200, url=https://httpbin.org/get?req=third

Total time: 1247ms (requests ran in parallel)
Sequential would take ~3000ms, we took ~1247ms

What happened here: sendAsync() returns a CompletableFuture<HttpResponse> immediately, allowing all three requests to run in parallel. We used Groovy’s collect to fire all requests, then collect again with join() to wait for results. The first request has a 1-second server delay, but because all three run concurrently, the total time is roughly the duration of the slowest request (about 1.2 seconds) rather than the sum of all three. This pattern is essential when your script needs to call multiple independent APIs – for example, fetching data from three microservices simultaneously. For more on concurrency in Groovy, see our Groovy Concurrency & GPars guide.

Example 12: Building a Simple REST Client Class

What we’re doing: Combining everything into a reusable REST client class that handles JSON, authentication, errors, and all HTTP methods.

Example 12: REST Client Class

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import groovy.json.JsonSlurper
import groovy.json.JsonOutput

class RestClient {
    HttpClient client
    String baseUrl
    Map<String, String> defaultHeaders = [:]
    JsonSlurper slurper = new JsonSlurper()

    RestClient(String baseUrl, Map opts = [:]) {
        this.baseUrl = baseUrl.replaceAll('/+$', '')
        this.client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(opts.timeout ?: 10))
            .followRedirects(HttpClient.Redirect.NORMAL)
            .build()
        if (opts.headers) {
            this.defaultHeaders.putAll(opts.headers)
        }
    }

    def get(String path, Map params = [:]) {
        def query = params ? '?' + params.collect { k, v ->
            "${URLEncoder.encode(k.toString(), 'UTF-8')}=${URLEncoder.encode(v.toString(), 'UTF-8')}"
        }.join('&') : ''
        return request('GET', "${path}${query}", null)
    }

    def post(String path, Map body) {
        return request('POST', path, body)
    }

    def put(String path, Map body) {
        return request('PUT', path, body)
    }

    def delete(String path) {
        return request('DELETE', path, null)
    }

    private def request(String method, String path, Map body) {
        def uri = URI.create("${baseUrl}${path}")
        def builder = HttpRequest.newBuilder().uri(uri)

        defaultHeaders.each { k, v -> builder.header(k, v) }
        builder.header('Accept', 'application/json')

        if (body != null) {
            def json = JsonOutput.toJson(body)
            builder.header('Content-Type', 'application/json')
            builder.method(method, HttpRequest.BodyPublishers.ofString(json))
        } else {
            builder.method(method, HttpRequest.BodyPublishers.noBody())
        }

        try {
            def resp = client.send(builder.build(), HttpResponse.BodyHandlers.ofString())
            def parsed = resp.body() ? slurper.parseText(resp.body()) : null
            return [status: resp.statusCode(), body: parsed, ok: resp.statusCode() in 200..299]
        } catch (Exception e) {
            return [status: 0, body: null, ok: false, error: e.message]
        }
    }
}

// --- Use the REST client ---
def api = new RestClient('https://httpbin.org', [
    headers: ['X-App': 'GroovyCookbook']
])

// GET
def getResult = api.get('/get', [search: 'groovy', page: 1])
println "GET: ok=${getResult.ok}, args=${getResult.body.args}"

// POST
def postResult = api.post('/post', [name: 'Nirranjan', role: 'Developer'])
println "POST: ok=${postResult.ok}, sent=${postResult.body.json}"

// PUT
def putResult = api.put('/put', [name: 'Nirranjan', role: 'Senior Developer'])
println "PUT: ok=${putResult.ok}, sent=${putResult.body.json}"

// DELETE
def delResult = api.delete('/delete')
println "DELETE: ok=${delResult.ok}, status=${delResult.status}"

// Error handling
def errResult = api.get('/status/404')
println "404: ok=${errResult.ok}, status=${errResult.status}"

Output

GET: ok=true, args=[search:groovy, page:1]
POST: ok=true, sent=[name:Nirranjan, role:Developer]
PUT: ok=true, sent=[name:Nirranjan, role:Senior Developer]
DELETE: ok=true, status=200
404: ok=false, status=404

What happened here: We built a production-style RestClient class that wraps java.net.http.HttpClient. It handles URL construction, JSON serialization/deserialization, default headers, error catching, and all four CRUD methods. The request() method returns a consistent map with status, body (already parsed from JSON), and an ok boolean. This class has zero external dependencies – it uses only Groovy’s built-in JSON support and the JDK’s HttpClient. You can extend it with retry logic, token refresh, rate limiting, or logging as needed.

A Note on HTTPBuilder

The groovyx.net.http.HTTPBuilder library (and its successor http-builder-ng) provides a Groovy DSL for HTTP operations. It’s worth knowing about, but with Java’s built-in HttpClient now available and Groovy’s GDK enhancements on URL, the need for an external HTTP library has diminished significantly. If you’re starting a new project, use the built-in tools shown in this post. If you maintain code that already uses HTTPBuilder, it still works fine – just be aware it’s a third-party dependency you can eventually remove.

Common Pitfalls

These are the mistakes that waste the most debugging time when doing HTTP in Groovy.

Pitfall 1: Reading errorStream on Failures

Pitfall: Wrong Stream on Error

// BAD - throws IOException on 4xx/5xx responses
def url = new URL('https://httpbin.org/status/404')
try {
    def body = url.text  // Throws!
} catch (FileNotFoundException e) {
    println "Caught: ${e.class.simpleName}"
}

// GOOD - use HttpURLConnection and check the status
HttpURLConnection conn = (HttpURLConnection) url.openConnection()
def code = conn.responseCode
def body = (code >= 400) ? conn.errorStream?.text : conn.inputStream.text
println "Status: ${code}, Body length: ${body?.length() ?: 0}"
conn.disconnect()

Pitfall 2: Forgetting to Set Timeouts

Pitfall: No Timeout

// BAD - no timeout, hangs forever if server is unresponsive
def conn = (HttpURLConnection) new URL('https://example.com/slow').openConnection()
def body = conn.inputStream.text  // Could block indefinitely

// GOOD - always set timeouts
def conn2 = (HttpURLConnection) new URL('https://example.com/slow').openConnection()
conn2.connectTimeout = 5000  // 5 seconds to establish connection
conn2.readTimeout = 10000    // 10 seconds to read response
try {
    def body2 = conn2.inputStream.text
} catch (SocketTimeoutException e) {
    println "Request timed out - server too slow"
}

Pitfall 3: Not Encoding Query Parameters

Pitfall: Unencoded Parameters

// BAD - special chars break the URL
def query = "name=O'Brien&city=New York"
def url = new URL("https://api.example.com/search?${query}")  // Malformed!

// GOOD - encode each parameter value
def params = [name: "O'Brien", city: 'New York']
def encoded = params.collect { k, v ->
    "${URLEncoder.encode(k, 'UTF-8')}=${URLEncoder.encode(v, 'UTF-8')}"
}.join('&')
def safeUrl = new URL("https://api.example.com/search?${encoded}")
println "Safe URL: ${safeUrl}"
// Safe URL: https://api.example.com/search?name=O%27Brien&city=New+York

Conclusion

Groovy gives you three tiers of HTTP capabilities without any external dependencies. For quick scripts and one-liners, URL.text is unbeatable – no other JVM language lets you fetch a URL in a single expression. When you need headers, authentication, or non-GET methods, HttpURLConnection with Groovy’s property syntax is clean and simple. And for modern applications that need HTTP/2, async operations, or a builder-based API, Java’s HttpClient works perfectly from Groovy code.

The REST client class in Example 12 shows how little code it takes to build a full-featured HTTP client in Groovy. You get JSON handling, error management, and all HTTP methods in under 60 lines – with zero external libraries. That’s the power of combining Groovy’s expressiveness with the JDK’s capabilities.

For related topics, see our Groovy JSON guide for deep JSON parsing and generation, Groovy XML for working with XML APIs, and Groovy Concurrency & GPars for parallel HTTP request patterns.

Up next: Groovy Number and Math Extensions

Best Practices

  • DO set connect and read timeouts on every connection – a missing timeout can hang your entire application.
  • DO use URL.text for quick scripts and prototyping where error handling isn’t critical.
  • DO URL-encode all query parameter values using URLEncoder.encode(value, 'UTF-8').
  • DO specify Content-Type and Accept headers explicitly – don’t rely on defaults.
  • DO reuse java.net.http.HttpClient instances – they’re thread-safe and manage connection pools.
  • DO read from errorStream (not inputStream) when the HTTP status is 4xx or 5xx.
  • DON’T use URL.text for production API calls – it provides no error handling, no timeout control, and no access to response headers.
  • DON’T forget to call disconnect() on HttpURLConnection – leaked connections exhaust the connection pool.
  • DON’T send credentials over plain HTTP – always use HTTPS for Basic Auth and API keys.
  • DON’T concatenate query parameters manually – always use URLEncoder to avoid injection and parsing bugs.

Frequently Asked Questions

How do I make a simple HTTP GET request in Groovy?

The simplest way is new URL('https://example.com').text which fetches the entire response body as a string in one line. Groovy adds a getText() method to java.net.URL through its GDK extensions. For more control over headers, timeouts, and error handling, use HttpURLConnection or java.net.http.HttpClient.

How do I send a POST request with JSON in Groovy?

Use HttpURLConnection: set requestMethod = 'POST', doOutput = true, and Content-Type to application/json. Convert your data to JSON with groovy.json.JsonOutput.toJson(yourMap) and write it to conn.outputStream. Alternatively, use java.net.http.HttpClient with HttpRequest.BodyPublishers.ofString(json) for a more modern API.

What is the difference between URL.text and HttpURLConnection in Groovy?

URL.text is a Groovy convenience that performs a GET request and returns the body as a string – no headers, no error handling, no timeout control. HttpURLConnection gives you full control over the HTTP method, request/response headers, timeouts, authentication, and error streams. Use URL.text for scripts and prototypes; use HttpURLConnection or HttpClient for production code.

How do I handle HTTP errors in Groovy?

With HttpURLConnection, check responseCode before reading the body. For 4xx and 5xx responses, read from errorStream instead of inputStream. Wrap the entire call in a try-catch to handle network errors like SocketTimeoutException, UnknownHostException, and ConnectException. With java.net.http.HttpClient, check response.statusCode() and handle exceptions from client.send().

Can I use java.net.http.HttpClient in Groovy?

Yes. Since Groovy runs on the JVM and requires Java 17+ (Groovy 5.x), java.net.http.HttpClient is fully available. It provides a modern, builder-based API with HTTP/2 support, async operations via CompletableFuture, and built-in redirect handling. It works naturally with Groovy syntax and requires no external dependencies.

Previous in Series: Groovy Concurrency and GPars

Next in Series: Groovy Number and Math Extensions

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *