New convenience HttpHandler

Ethan McCue ethan at mccue.dev
Mon Apr 28 22:48:25 UTC 2025


nevermind - i see the file server does read and discard any incoming bodies.

On Mon, Apr 28, 2025 at 5:52 PM Ethan McCue <ethan at mccue.dev> wrote:

> 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/68b5b33b/attachment-0001.htm>


More information about the net-dev mailing list