-
-
Save xenoterracide/7f7675ac827c9f6b0b193e2bb3dc5c5c to your computer and use it in GitHub Desktop.
java single source http client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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