Networking & HTTP

Sending Requests & Handling Responses

15 min Lesson 5 of 13

Sending Requests & Handling Responses

The previous lesson introduced HttpClient and its builder. Now we put it to work: crafting GET and POST requests, attaching headers, supplying a request body, and correctly interpreting the HttpResponse you get back. These are the everyday mechanics of any HTTP-based integration.

Building an HttpRequest

Every HTTP call starts with an HttpRequest built through its fluent builder. The minimum you supply is a URI and an HTTP method; everything else is optional but often essential in practice.

import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/products/42")) .GET() // explicit, though GET is the default .header("Accept", "application/json") .header("X-Request-ID", "req-abc-123") .build();
HttpRequest is immutable. Once built it cannot be mutated, which means it is safe to share across threads and to reuse for replay or retry logic. Never assume you need to rebuild a request object on every retry — you can send the same instance multiple times.

Sending a GET Request

Pass the request and a body handler to client.send(). The body handler tells the client how to convert the raw response bytes into the type you want:

HttpResponse<String> response = client.send( request, HttpResponse.BodyHandlers.ofString() // decode bytes as UTF-8 String ); System.out.println("Status : " + response.statusCode()); System.out.println("Body : " + response.body());

Common built-in body handlers include ofString(), ofBytes(), ofFile(Path), ofInputStream(), and discarding() (for responses where the body is irrelevant). Choose the one that matches what you intend to do with the data — streaming a large download to a file with ofFile() avoids loading gigabytes into heap memory.

Reading Response Metadata

HttpResponse<T> surfaces more than just the body:

int status = response.statusCode(); // 200, 404, 500 … String body = response.body(); HttpHeaders headers = response.headers(); // Get a single header value (lowercased name per HTTP/2 spec) String contentType = headers.firstValue("content-type").orElse("unknown"); // Get all values for a header (e.g. Set-Cookie can appear multiple times) java.util.List<String> cookies = headers.allValues("set-cookie"); // The URI after any redirects URI finalUri = response.uri(); // Which version was negotiated (HTTP_1_1 or HTTP_2) System.out.println(response.version());
Always check the status code before trusting the body. A 4xx or 5xx response often returns an error payload rather than the data you expect. Write a small helper that throws a meaningful exception for non-2xx status codes so callers do not silently process error JSON as if it were success data.

Sending a POST Request with a JSON Body

POST requests supply a body publisher that serialises your payload into bytes the client can stream to the server. The most common case is a JSON string:

String json = """ { "name": "Wireless Keyboard", "price": 49.99, "stock": 200 } """; HttpRequest postRequest = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/products")) .header("Content-Type", "application/json") .header("Accept", "application/json") .header("Authorization", "Bearer eyJhbGciOiJSUzI1NiJ9...") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse<String> postResponse = client.send( postRequest, HttpResponse.BodyHandlers.ofString() ); if (postResponse.statusCode() == 201) { System.out.println("Resource created: " + postResponse.body()); } else { System.err.println("Unexpected status: " + postResponse.statusCode()); System.err.println("Error body: " + postResponse.body()); }

For a POST with no body (uncommon but valid, e.g. triggering an action endpoint), use HttpRequest.BodyPublishers.noBody().

Other Built-in Body Publishers

  • BodyPublishers.ofString(String) — UTF-8 encoded text; the workhorse for JSON.
  • BodyPublishers.ofByteArray(byte[]) — raw bytes; useful for binary protocols or pre-serialised data.
  • BodyPublishers.ofFile(Path) — streams a file directly from disk without loading it into memory first; ideal for file-upload endpoints.
  • BodyPublishers.ofInputStream(Supplier<InputStream>) — lazily supplies an input stream; supports large or dynamically generated payloads.
  • BodyPublishers.noBody() — signals an explicitly empty body (Content-Length: 0).

Handling HTTP Status Codes Professionally

HTTP defines status codes in five classes. Understanding the classes — not just memorising individual numbers — is what lets you write robust error handling:

  • 1xx Informational — protocol-level signals (e.g. 100 Continue). Handled automatically by the client; you rarely see them.
  • 2xx Success200 OK (GET/PUT), 201 Created (POST), 204 No Content (DELETE with no body).
  • 3xx RedirectionHttpClient follows redirects automatically when configured with followRedirects(HttpClient.Redirect.NORMAL).
  • 4xx Client Error — the request is wrong. 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 429 Too Many Requests.
  • 5xx Server Error — the server failed. 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable.

A concise helper that encodes this logic:

private static <T> T requireSuccess(HttpResponse<T> response) { int status = response.statusCode(); if (status >= 200 && status < 300) { return response.body(); } throw new RuntimeException( "HTTP %d from %s".formatted(status, response.uri()) ); }
204 No Content means the body is empty by contract. If you use BodyHandlers.ofString() for a DELETE that returns 204, response.body() will be an empty string — not null. Passing an empty string to a JSON parser will throw. Check the status code before deserialising, or use BodyHandlers.discarding() when you know the body will be empty.

Setting Headers Correctly

Headers fall into a few categories worth knowing explicitly:

  • Content-Type — describes the body you are sending. Always set it on POST/PUT/PATCH. The most common values are application/json and application/x-www-form-urlencoded.
  • Accept — tells the server what format you can parse. Providing application/json is explicit and avoids surprises with APIs that can return XML or HTML.
  • Authorization — carries credentials. Use Bearer <token> for OAuth 2.0 / JWT, or Basic <base64> for HTTP Basic auth. Never pass credentials as query parameters.
  • User-Agent — identifies your client. Some APIs rate-limit or block requests without a meaningful user agent string.
  • Idempotency-Key (Stripe, etc.) — a unique UUID you generate per logical operation so the server can safely deduplicate retried POST requests.
// Setting multiple headers cleanly HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.stripe.com/v1/charges")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/x-www-form-urlencoded") .header("Idempotency-Key", java.util.UUID.randomUUID().toString()) .header("User-Agent", "MyApp/2.1 (Java/" + System.getProperty("java.version") + ")") .POST(HttpRequest.BodyPublishers.ofString("amount=2000&currency=usd&source=tok_visa")) .build();

Summary

You now know how to craft both GET and POST requests, attach arbitrary headers, choose the right body publisher for your payload, and read back the status code, headers, and body of the response. These are the building blocks every HTTP interaction in Java is composed of — the async and REST-client patterns in the next lessons build directly on this foundation.