HttpRequest.BodyPublishers.ofString()
Send a String as the request body for POST, PUT or PATCH, with the right Content-Type and a byte-accurate Content-Length.
static HttpRequest.BodyPublisher ofString(String s)- Added in Java Java 11
Parameters
| Parameter | Type | Description |
|---|---|---|
$s | String | The text to send as the request body. |
$charsetoptional | Charset= StandardCharsets.UTF_8 | Only in the two-argument overload: the charset used to encode the string. |
Return value
HttpRequest.BodyPublisherA body publisher that sends the string encoded as UTF-8, with a known content length in bytes. The overload ofString(String s, Charset charset) encodes with the given charset.
Errors
NullPointerException if s or charset is null.
HttpRequest.BodyPublishers.ofString(s) turns a String into a request body. Pass it to POST, PUT or method("PATCH", ...) on the request builder. The text is encoded as UTF-8, or with the charset you pass to ofString(s, charset).
HttpRequest request = HttpRequest.newBuilder(uri)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
It does not set Content-Type
This is the mistake almost everyone makes once. The publisher supplies bytes and a length, nothing else. Without a Content-Type header the server does not know the body is JSON; a framework typically answers 415 Unsupported Media Type, or binds nothing and fails validation with a confusing message. Always add header("Content-Type", "application/json"), or the form type for form posts.
Content-Length is in bytes
The publisher knows its length up front, so the client sends a Content-Length header instead of chunked encoding. The length counts encoded bytes, not characters: "héllo" is five characters but six bytes in UTF-8. You never set this header yourself; the builder rejects it.
Charsets
UTF-8 is right for JSON, which RFC 8259 requires for documents exchanged between systems. Use the two-argument form only when the server needs something else, and then say so in the header too: text/plain; charset=ISO-8859-1. A body encoded one way and labelled another is how names with accents turn into question marks in someone's database.
Building the string
Producing JSON by concatenating strings is fine for fixed shapes, but any value from a user must be escaped, or a quote in a name breaks the document. In real services a JSON library such as Jackson writes the string. For HTML-style forms, join key=value pairs with &, encode each part with URLEncoder.encode(value, StandardCharsets.UTF_8), and send application/x-www-form-urlencoded.
Other publishers
noBody() for an empty POST, ofByteArray(bytes) for binary data, ofFile(path) to stream an upload from disk without loading it into memory, and ofInputStream(supplier) when the length is unknown.
Example
When to use it
- Creating an order by POSTing a JSON document to another service and reading the Location header of the 201.
- Replacing a user's profile with a PUT of the full JSON representation.
- Requesting an OAuth access token by posting client_credentials as a form body.
Frequently asked questions
How do I POST JSON with Java HttpClient?
Build the request with header("Content-Type", "application/json") and POST(HttpRequest.BodyPublishers.ofString(json)), then call client.send(request, HttpResponse.BodyHandlers.ofString()). The JSON string itself usually comes from a library such as Jackson.
Does BodyPublishers.ofString set the Content-Type header?
No. It only provides the bytes and the content length. Set Content-Type yourself on the request builder.
Which charset does BodyPublishers.ofString use?
UTF-8. Use ofString(text, charset) for a different encoding, and name the same charset in the Content-Type header so the server decodes it correctly.
More examples
Forgetting Content-Type
The two requests carry identical bytes; only the header differs. The server here mimics what framework controllers do with an unlabelled body.
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
// Writes a small text response; an empty body is sent with no body at all.
void reply(HttpExchange exchange, int status, String contentType, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
if (contentType != null) {
exchange.getResponseHeaders().set("Content-Type", contentType);
}
exchange.sendResponseHeaders(status, bytes.length == 0 ? -1 : bytes.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
}
void main() throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
server.createContext("/users", exchange -> {
String type = exchange.getRequestHeaders().getFirst("Content-Type");
exchange.getRequestBody().readAllBytes();
if (type == null || !type.startsWith("application/json")) {
reply(exchange, 415, "text/plain", "expected application/json, got " + type);
} else {
reply(exchange, 201, "text/plain", "created");
}
});
server.start();
URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/users");
String json = "{\"name\":\"Ada\"}";
try (HttpClient client = HttpClient.newHttpClient()) {
HttpRequest bare = HttpRequest.newBuilder(uri)
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpRequest labelled = HttpRequest.newBuilder(uri)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
for (HttpRequest request : List.of(bare, labelled)) {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
IO.println(response.statusCode() + " " + response.body());
}
} finally {
server.stop(0);
}
}
// Output:
// 415 expected application/json, got null
// 201 createdContent-Length counts bytes, not characters
<code>contentLength()</code> is the value the client sends as <code>Content-Length</code>. The euro sign has no ISO-8859-1 form, so it is replaced by a single <code>?</code> byte: the length looks fine and the data is silently wrong.
import java.net.http.HttpRequest;
void main() {
for (String text : List.of("hello", "héllo", "€10", "")) {
long utf8 = HttpRequest.BodyPublishers.ofString(text).contentLength();
long latin1 = HttpRequest.BodyPublishers.ofString(text, StandardCharsets.ISO_8859_1).contentLength();
IO.println(text.length() + " chars, UTF-8 " + utf8 + " bytes, ISO-8859-1 " + latin1 + " bytes");
}
IO.println("noBody: " + HttpRequest.BodyPublishers.noBody().contentLength());
}
// Output:
// 5 chars, UTF-8 5 bytes, ISO-8859-1 5 bytes
// 5 chars, UTF-8 6 bytes, ISO-8859-1 5 bytes
// 3 chars, UTF-8 5 bytes, ISO-8859-1 3 bytes
// 0 chars, UTF-8 0 bytes, ISO-8859-1 0 bytes
// noBody: 0Posting an HTML-style form
Token endpoints and older APIs expect form bodies, not JSON. Each key and value is encoded separately, so the space and colons inside <code>scope</code> cannot be confused with the <code>&</code> and <code>=</code> separators.
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
// Writes a small text response; an empty body is sent with no body at all.
void reply(HttpExchange exchange, int status, String contentType, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
if (contentType != null) {
exchange.getResponseHeaders().set("Content-Type", contentType);
}
exchange.sendResponseHeaders(status, bytes.length == 0 ? -1 : bytes.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
}
String form(Map<String, String> fields) {
return fields.entrySet().stream()
.map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8) + "="
+ URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}
void main() throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
server.createContext("/oauth/token", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
reply(exchange, 200, "text/plain", exchange.getRequestHeaders().getFirst("Content-Type") + " | " + body);
});
server.start();
URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/oauth/token");
Map<String, String> fields = new LinkedHashMap<>();
fields.put("grant_type", "client_credentials");
fields.put("scope", "orders:read orders:write");
HttpRequest request = HttpRequest.newBuilder(uri)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(form(fields)))
.build();
try (HttpClient client = HttpClient.newHttpClient()) {
IO.println(client.send(request, HttpResponse.BodyHandlers.ofString()).body());
} finally {
server.stop(0);
}
}
// Output:
// application/x-www-form-urlencoded | grant_type=client_credentials&scope=orders%3Aread+orders%3Awrite
Discussion