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.”

Last Updated: July 2026 | Tested on: Groovy 5.0.6, 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.

The diagram below shows the response handling flow every Groovy HTTP call follows: after you read the status code, it decides which stream to read from, so 2xx responses go to inputStream while 4xx and 5xx errors go to errorStream, with network failures caught separately:

No (timeout, DNS,refused)Yes200 to 299300 to 399(only iffollowRedirects=false)400 to 599Open HttpURLConnectionand read responseCodeNetwork callsucceeded?catch IOExceptionSocketTimeoutException,UnknownHostExceptionStatus coderange?Read conn.inputStream.textRead Location header(auto-followed by default)Read conn.errorStream.textParse body with JsonSlurperconn.disconnect()Groovy HTTP Response Handling: Which Stream to Read by Status Code

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

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)

Groovy HTTP REST 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 in groovy HTTP REST code.

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 HTTP REST calls come in three tiers, and none of them needs an external dependency. 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, the two workhorses of Groovy HTTP REST code.

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.0.6), 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.

Interview Questions on Groovy HTTP Clients

These come from real screens. Practice answering before you read each answer.

Q: A nightly Jenkins job that polls an internal API with new URL(u).text hangs forever about once a month and blocks the whole pipeline. Diagnosis and fix?

Classic missing timeout. URL.text exposes no timeout control, and the JDK default is to wait indefinitely, so one stalled TCP connection parks the thread until someone kills the build. The fix is to drop to openConnection() and set both connectTimeout and readTimeout, or use java.net.http.HttpClient with a connect timeout on the client and a per-request timeout on the builder. Then wrap the call in retry-with-backoff so a transient stall degrades to a logged retry instead of a hung pipeline. The gotcha interviewers want named: infinite defaults.

Q: Your script logs FileNotFoundException when a REST endpoint returns 404 with a useful JSON error body. Why that exception, and how do you actually capture the body?

HttpURLConnection.getInputStream() throws on 4xx and 5xx, and for 404 specifically it surfaces as FileNotFoundException, which confuses everyone the first time. The server’s error body is sitting on errorStream, a completely separate stream. The pattern: read responseCode first, then choose inputStream for 2xx and errorStream otherwise, and null-check errorStream because some errors have no body. Worth adding that java.net.http.HttpClient removes the trap entirely: it hands you the response object for any status code.

Q: A teammate builds query strings with GString interpolation, "?q=${term}", and a search for “cats & dogs” silently returns wrong results. What happened, and what is the robust fix?

The ampersand in the raw value acts as a parameter separator, so the server sees q=cats plus a junk parameter named dogs, and nothing errors: you just get wrong data. Every value must go through URLEncoder.encode(value, 'UTF-8') before concatenation. The robust version is a small helper that takes a map and produces the encoded query string, so encoding cannot be forgotten at any call site. Bonus point in an interview: URLEncoder does form encoding, so spaces become +, which servers accept in query strings.

Q: A Groovy script calls three independent microservices sequentially and takes over three seconds. How do you speed it up without adding a single library?

Fire the calls concurrently with the JDK’s own client: HttpClient.sendAsync() returns a CompletableFuture immediately, so you collect the three requests into futures, then join() each one. Total latency drops to roughly the slowest single call. Use one shared HttpClient for all three, since it is thread-safe and pools connections, and put a timeout on every request so one slow service cannot drag the batch. On Java 21+, virtual threads with plain blocking calls achieve the same effect with even simpler code.

Q: Under load testing, a Groovy service that builds a new HttpClient for every request eventually dies with socket exhaustion. Why, and what is the correct lifecycle?

Each HttpClient owns its own connection pool and executor, so a client per request means a pool per request: connections never get reused, sockets pile up in TIME_WAIT, and the process runs out of file descriptors. The correct lifecycle is build once, share everywhere: the client is immutable and thread-safe by design, exactly so it can be a long-lived singleton. Per-request settings like timeouts belong on the HttpRequest builder, not on a fresh client. The same discipline applies to HttpURLConnection: read streams fully so keep-alive can recycle the socket.

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 *