NanoHttpd / nanohttpd

Tiny, easily embeddable HTTP server in Java.
http://nanohttpd.org
BSD 3-Clause "New" or "Revised" License
6.89k stars 1.69k forks source link

How to stream a page instead of building entire strings and delivering them #632

Open Phlip opened 1 year ago

Phlip commented 1 year ago

I use JATL HTML builder to build web pages. It takes a stream and injects HTML tags into it.

This means that to serve a web page, NanoHTTPD calls my .serve() method, and I build a string, and return the entire string for NanoHTTPD to then copy into its output stream.

If I can instead pass NanoHTTPD's output socket directly into the HTML builder, then the user's web browser can be rendering the top part of a web page /while/ the page builder is still building the bottom part. This is tons more efficient, and it doesn't require buffering a large string in memory.

This tweak generates the HTTP header and then passes the output stream directly to the .serve() handler:

    public final void sender(@NonNull IHTTPSession session, @NonNull Consumer<Writer> run) {
        OutputStream outputStream = session.getOutputStream();
        SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
        gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));

        try {
            if (status == null) {
                throw new Error("sendResponse(): Status can't be null.");
            }
            String encoding = new ContentType(mimeType).getEncoding();
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(outputStream, encoding), 4096);  //  4096 because that's one memory page on all available architectures
            PrintWriter pw = new PrintWriter(out, false);
            pw.append("HTTP/1.1 ").append(status.getDescription()).append(" \r\n");
            if (mimeType != null) {
                printHeader(pw, "Content-Type", mimeType);
            }
            if (getHeader("date") == null) {
                printHeader(pw, "Date", gmtFrmt.format(new Date()));
            }
            for (Entry<String, String> entry : header.entrySet()) {
                printHeader(pw, entry.getKey(), entry.getValue());
            }
            for (String cookieHeader : cookieHeaders) {
                printHeader(pw, "Set-Cookie", cookieHeader);
            }
//            if (getHeader("connection") == null) {  // CONSIDER  remove this at the source
//                printHeader(pw, "Connection", (keepAlive ? "keep-alive" : "close"));
//            }
//            if (getHeader("content-length") != null) {
//                setUseGzip(false);
//            }
//            if (useGzipWhenAccepted()) {
//                printHeader(pw, "Content-Encoding", "gzip");
//                setChunkedTransfer(true);
//            }
            long pending = data != null ? contentLength : 0;
            if (requestMethod != Method.HEAD && chunkedTransfer) {
                printHeader(pw, "Transfer-Encoding", "chunked");
            } /*else if (!useGzipWhenAccepted()) {
                pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending);
            } */
            pw.append("\r\n");
            pw.flush();
            run.accept(pw);  //  <-- your code builds the page here
            pw.flush();
            outputStream.flush();
            outputStream.close();  //  we can't figure out safeClose(), and the user agent awaits this, so we do it here
            NanoHTTPD.safeClose(data);
        } catch (IOException ioe) {
            NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe);
        }
    }

(The patch also contains commented code for a few features we hacked out.)

Here's an example of the calling code inside .serve():

    @Override
    public Response serve(@NonNull IHTTPSession session) {
        Response response = newFixedLengthResponse(Status.CONFLICT, NanoHTTPD.MIME_HTML + "; charset=UTF-8", "");

        response.sender(session,
                (output) -> new HTML(output).em().raw("Can't serve web pages while busy.").end() );

        return null;  //  this tells the default handler that we handled the page and it has nothing to do
    }

You can see that if the HTML() result was much longer, such as a complete report, this is more efficient than serving a string.

So anyone can throw this patch in if they want it, and the NanoHTTPD maintainers could consider productizing it and adding it to the latest release, right?

(Another suggestion would be to advise the big web server systems, such as Django and Ruby on Rails, that they should stream, too, instead of serving entire strings!;)