Skip to content

Instantly share code, notes, and snippets.

@xenoterracide
Created February 2, 2026 23:13
Show Gist options
  • Select an option

  • Save xenoterracide/7f7675ac827c9f6b0b193e2bb3dc5c5c to your computer and use it in GitHub Desktop.

Select an option

Save xenoterracide/7f7675ac827c9f6b0b193e2bb3dc5c5c to your computer and use it in GitHub Desktop.
java single source http client
#!/usr/bin/env -S java --source 21
// SPDX-FileCopyrightText: Copyright © 2026 Caleb Cushing
//
// SPDX-License-Identifier: Apache-2.0
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.SequenceInputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
class JGet {
public static void main(String... args) {
Args parsed = Args.parse(args);
var clientBuilder = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.ALWAYS)
.connectTimeout(Duration.ofSeconds(20));
try( var client = clientBuilder.build()) {
var request = HttpRequest.newBuilder()
.uri(URI.create(parsed.url))
.timeout(Duration.ofMinutes(1))
.method(parsed.method.toUpperCase(), HttpRequest.BodyPublishers.noBody() )
.build();
try {
var response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
try (var stream = response.body()) {
var prefix = readPrefix(stream, 4096);
var binary = isBinary(response, prefix);
if (binary && isTty()) {
System.err.println("refusing to print binary response to terminal");
System.exit(Args.ERROR_EXIT_CODE);
}
var combined = new SequenceInputStream(
new ByteArrayInputStream(prefix),
stream
);
if (binary) {
combined.transferTo(System.out);
} else {
var charset = resolveCharset(response);
try (var reader = new InputStreamReader(combined, charset)) {
var buffer = new char[8192];
int read;
while ((read = reader.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, read));
}
}
}
}
} catch (java.io.IOException exception) {
System.err.println(exception.getMessage());
System.exit(Args.ERROR_EXIT_CODE);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
System.err.println(exception.getMessage());
System.exit(Args.ERROR_EXIT_CODE);
}
}
}
static boolean isTty() {
return System.console() != null;
}
static boolean isBinary(HttpResponse<?> response, byte[] prefix) {
var contentType = response.headers().firstValue("Content-Type").orElse("");
if (!contentType.isBlank()) {
return !isTextualContentType(contentType);
}
return looksBinary(prefix);
}
static boolean isTextualContentType(String contentType) {
var mediaType = contentType.split(";", 2)[0].trim().toLowerCase(Locale.ROOT);
return mediaType.startsWith("text/")
|| mediaType.equals("application/json")
|| mediaType.equals("application/xml")
|| mediaType.equals("application/javascript")
|| mediaType.equals("application/x-www-form-urlencoded")
|| mediaType.endsWith("+json")
|| mediaType.endsWith("+xml");
}
static Charset resolveCharset(HttpResponse<?> response) {
var contentType = response.headers().firstValue("Content-Type").orElse("");
for (var part : contentType.split(";")) {
var trimmed = part.trim();
if (trimmed.toLowerCase(Locale.ROOT).startsWith("charset=")) {
var value = trimmed.substring("charset=".length()).trim();
if (!value.isEmpty()) {
try {
return Charset.forName(value);
} catch (IllegalArgumentException ignored) {
return StandardCharsets.UTF_8;
}
}
}
}
return StandardCharsets.UTF_8;
}
static boolean looksBinary(byte[] body) {
var limit = Math.min(body.length, 1024);
if (limit == 0) {
return false;
}
var nonPrintable = 0;
for (var i = 0; i < limit; i++) {
var b = body[i] & 0xFF;
if (b == 0) {
return true;
}
if (b < 0x09 || (b > 0x0D && b < 0x20)) {
nonPrintable++;
}
}
return nonPrintable > limit / 5;
}
static byte[] readPrefix(InputStream stream, int maxBytes) throws java.io.IOException {
var buffer = new byte[maxBytes];
var total = 0;
while (total < maxBytes) {
var read = stream.read(buffer, total, maxBytes - total);
if (read == -1) {
break;
}
total += read;
}
if (total == maxBytes) {
return buffer;
}
var trimmed = new byte[total];
System.arraycopy(buffer, 0, trimmed, 0, total);
return trimmed;
}
}
final class Args {
static final int ERROR_EXIT_CODE = 2;
static final String EXPECTED_METHOD_URL = "expected METHOD URL";
final String method;
final String url;
private Args(String method, String url) {
this.method = method;
this.url = url;
}
static Args parse(String... args) {
var method = (String) null;
var url = (String) null;
for (String arg : args) {
switch (arg) {
case "-h", "--help" -> printUsageAndExit(System.out, 0);
default -> {
if (method == null) {
method = arg;
} else if (url == null) {
url = arg;
} else {
// Extra positional arguments beyond METHOD and URL.
die(EXPECTED_METHOD_URL);
}
}
}
}
if (method == null || url == null) {
// Missing required positional arguments.
die(EXPECTED_METHOD_URL);
}
if (!"get".equalsIgnoreCase(method)) {
die("unsupported method: " + method);
}
return new Args(method, url);
}
static void printUsageAndExit(PrintStream stream, int code) {
stream.println("usage: jget METHOD URL");
stream.println("write file: jget METHOD URL > output.bin");
System.exit(code);
}
static void die(String message) {
System.err.println(message);
printUsageAndExit(System.err, ERROR_EXIT_CODE);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment