New convenience HttpHandler
Ethan McCue
ethan at mccue.dev
Mon Apr 28 21:52:25 UTC 2025
Sorry, forgot reply-all
On Mon, Apr 28, 2025 at 5:52 PM Ethan McCue <ethan at mccue.dev> wrote:
> Today you would need to do the following:
>
> exchange -> {
> var body = "example".getBytes();
> exchange.sendResponseHeaders(200, body.length);
> try (var os = exchange.getResponseBody()) {
> os.write(body);
> }
> }
>
> I don't think you need to read the request body, but I might be wrong
> about that. If so, I have some thinking to do. (and FileServerHandler
> needs to be fixed)
>
> And if you weren't doing a static value, you'd want to do this.
>
> exchange -> {
> var body = compute().getBytes();
> exchange.sendResponseHeaders(200, body.length == 0 ? -1 : body.length);
> try (var os = exchange.getResponseBody()) {
> os.write(body);
> }
> }
>
> Where the body.length == 0 ? -1 : body.length is for technical
> correctness in case the body is length zero.
>
> Essentially, I think something approximately like what I have here should
> be integrated. https://github.com/bowbahdoe/jdk-httpserver
>
> The reason I'm not pushing on it is:
>
> * The design has a ResponseLength.known(..) and ResponseLength.unknown() which
> handle that 0 ? -1 nonsense, but that only works with corresponding patterns
> * ResponseLength should be value class
> * The Body interface has a defaultContentType which scratches at the back
> of my brain - Could a Body imply additional headers? Is it worth having
> just for JsonBody.of(...);?
>
> But essentially, this is the meat of it.
>
> exchange -> HttpExchanges.sendResponse(exchange, 200, Body.of("example"));
>
> interface Body {
> void writeTo(OutputStream outputStream) throws IOException;
>
> default ResponseLength responseLength() {
> return ResponseLength.unknown();
> }
>
> default Optional<String> defaultContentType() {
> return Optional.empty();
> }
>
> // ...
> }
>
> public static void sendResponse(
> HttpExchange exchange, int rCode, Body body
> ) throws IOException {
> body.defaultContentType()
> .ifPresent(contentType ->
> exchange.getResponseHeaders().putIfAbsent(
> "Content-Type",
> List.of(contentType)
> ));
>
> sendResponseHeaders(exchange, rCode, body.responseLength());
> try (var os = exchange.getResponseBody()) {
> body.writeTo(os);
> }
> }
>
> On Mon, Apr 28, 2025 at 5:36 PM Pavel Rappo <pavel.rappo at gmail.com> wrote:
>
>> You are right that a function would be more general than a supplier.
>> That crossed my mind too. However, here, with greater generality comes
>> less convenience. Like with your example, don't you need to read a
>> request body and then finally close the exchange? Perhaps that can be
>> solved with better documentation rather than one more convenience
>> method. Perhaps documentation could provide (a snippet of) a skeletal
>> implementation of a handler.
>>
>> Also, even the "static string" convenience handler is problematic: no
>> one sets the content type because it's unknown: the passed string can
>> be HTML, JSON, plain text or what have you. And no one sets the
>> character encoding because (AFAIK) it only comes with the content type
>> and not on its own.
>>
>> On Mon, Apr 28, 2025 at 8:53 PM Ethan McCue <ethan at mccue.dev> wrote:
>> >
>> > My first thought is that if you are computing the string maybe you'd
>> want to look at the request. Or set a header for content type.
>> >
>> > I'm not going to push hard until valhalla is at least in preview (+
>> we've already talked it to death) but a Body abstraction would probably
>> solve your use-case more robustly than yet another method there.
>> >
>> > exchange -> exchange.sendResponse(200, Body.of("example"));
>> >
>> > On Mon, Apr 28, 2025 at 5:28 AM Pavel Rappo <pavel.rappo at gmail.com>
>> wrote:
>> >>
>> >> I'm using HttpServer to implement an HTTP probe [^1] that provides
>> >> application state at the time of probing. I find that convenience
>> >> handlers provided by HttpHandlers are insufficient for my use case. I
>> >> also find that implementing a custom HttpHandler is tricky without the
>> >> help of documentation.
>> >>
>> >> Perhaps my use case is typical enough so that HttpHandlers could
>> >> provide a new convenience handler. That handler would send a
>> >> dynamically supplied string, rather than a static string. If you also
>> >> find it useful, I'd appreciate it if we could discuss it. Before I
>> >> create a JBS issue, please have a look at this crude patch to see if
>> >> it makes sense to you in principle. Thanks.
>> >>
>> >> [^1]:
>> https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
>> >>
>> >> diff --git
>> a/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpHandlers.java
>> >>
>> b/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpHandlers.java
>> >> index 03642033914..987de0ede5d 100644
>> >> ---
>> a/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpHandlers.java
>> >> +++
>> b/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpHandlers.java
>> >> @@ -25,9 +25,11 @@
>> >>
>> >> package com.sun.net.httpserver;
>> >>
>> >> +import java.io.OutputStream;
>> >> import java.nio.charset.StandardCharsets;
>> >> import java.util.Objects;
>> >> import java.util.function.Predicate;
>> >> +import java.util.function.Supplier;
>> >>
>> >> /**
>> >> * Implementations of {@link com.sun.net.httpserver.HttpHandler
>> HttpHandler}
>> >> @@ -140,28 +142,60 @@ public static HttpHandler
>> >> handleOrElse(Predicate<Request> handlerTest,
>> >> * @throws NullPointerException if headers or body are null
>> >> */
>> >> public static HttpHandler of(int statusCode, Headers headers,
>> >> String body) {
>> >> + Objects.requireNonNull(body);
>> >> + return of(statusCode, headers, () -> body);
>> >> + }
>> >> +
>> >> + /**
>> >> + * Returns an {@code HttpHandler} that sends a response
>> >> comprising the given
>> >> + * {@code statusCode}, {@code headers}, and {@code body}.
>> >> + *
>> >> + * <p> This method creates a handler that reads and discards the
>> request
>> >> + * body before it sets the response state and sends the response.
>> >> + *
>> >> + * <p> {@code headers} are the effective headers of the response.
>> The
>> >> + * response <i>body bytes</i> are a {@code UTF-8} encoded byte
>> sequence of
>> >> + * a string, which is supplied by {@code bodySupplier}
>> >> immediately after the request body is read. The response headers
>> >> + * {@linkplain HttpExchange#sendResponseHeaders(int, long) are
>> sent} with
>> >> + * the given {@code statusCode} and the body bytes' length (or
>> {@code -1}
>> >> + * if the body is empty). The body bytes are then sent as
>> response body,
>> >> + * unless the body is empty, in which case no response body is
>> sent.
>> >> + *
>> >> + * @param statusCode a response status code
>> >> + * @param headers a headers
>> >> + * @param bodySupplier a supplier for the response body string
>> >> + * @return a handler
>> >> + * @throws IllegalArgumentException if statusCode is not a
>> positive 3-digit
>> >> + * integer, as per rfc2616,
>> section 6.1.1
>> >> + * @throws NullPointerException if headers or body are null
>> >> + */
>> >> + public static HttpHandler of(int statusCode, Headers headers,
>> >> Supplier<String> bodySupplier) {
>> >> if (statusCode < 100 || statusCode > 999)
>> >> throw new IllegalArgumentException("statusCode must be
>> 3-digit: "
>> >> + statusCode);
>> >> Objects.requireNonNull(headers);
>> >> - Objects.requireNonNull(body);
>> >> + Objects.requireNonNull(bodySupplier);
>> >>
>> >> final var headersCopy = Headers.of(headers);
>> >> - final var bytes = body.getBytes(StandardCharsets.UTF_8);
>> >>
>> >> return exchange -> {
>> >> try (exchange) {
>> >> - exchange.getRequestBody().readAllBytes();
>> >> +
>> >> exchange.getRequestBody().transferTo(OutputStream.nullOutputStream());
>> >> // discard
>> >> exchange.getResponseHeaders().putAll(headersCopy);
>> >> - if (exchange.getRequestMethod().equals("HEAD")) {
>> >> -
>> >> exchange.getResponseHeaders().set("Content-Length",
>> >> Integer.toString(bytes.length));
>> >> - exchange.sendResponseHeaders(statusCode, -1);
>> >> - }
>> >> - else if (bytes.length == 0) {
>> >> - exchange.sendResponseHeaders(statusCode, -1);
>> >> + var body = bodySupplier.get();
>> >> + if (body == null) {
>> >> + exchange.sendResponseHeaders(500, -1); //
>> >> Internal Server Error
>> >> } else {
>> >> - exchange.sendResponseHeaders(statusCode,
>> bytes.length);
>> >> - exchange.getResponseBody().write(bytes);
>> >> + final var bytes =
>> body.getBytes(StandardCharsets.UTF_8);
>> >> + if (exchange.getRequestMethod().equals("HEAD")) {
>> >> +
>> >> exchange.getResponseHeaders().set("Content-Length",
>> >> Integer.toString(bytes.length));
>> >> + exchange.sendResponseHeaders(statusCode, -1);
>> >> + } else if (bytes.length == 0) {
>> >> + exchange.sendResponseHeaders(statusCode, -1);
>> >> + } else {
>> >> + exchange.sendResponseHeaders(statusCode,
>> bytes.length);
>> >> + exchange.getResponseBody().write(bytes);
>> >> + }
>> >> }
>> >> }
>> >> };
>> >> diff --git
>> a/test/jdk/com/sun/net/httpserver/simpleserver/HttpHandlersTest.java
>> >> b/test/jdk/com/sun/net/httpserver/simpleserver/HttpHandlersTest.java
>> >> index 85d271e44fa..d64fa03740f 100644
>> >> ---
>> a/test/jdk/com/sun/net/httpserver/simpleserver/HttpHandlersTest.java
>> >> +++
>> b/test/jdk/com/sun/net/httpserver/simpleserver/HttpHandlersTest.java
>> >> @@ -81,7 +81,7 @@ public void testNull() {
>> >> final var headers = new Headers();
>> >> final var body = "";
>> >> assertThrows(NPE, () -> HttpHandlers.of(200, null, body));
>> >> - assertThrows(NPE, () -> HttpHandlers.of(200, headers, null));
>> >> + assertThrows(NPE, () -> HttpHandlers.of(200, headers,
>> (String) null));
>> >> }
>> >>
>> >> @Test
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/net-dev/attachments/20250428/631f9e41/attachment-0001.htm>
More information about the net-dev
mailing list