From 22e8fa9b7060baf50a2f3177c960dc393cd7e580 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Sun, 27 Jul 2025 11:14:24 -0400 Subject: [PATCH] Fixed flushing of empty responses. --- source/handy_http_transport/http1/task_pool.d | 8 +++++++- source/handy_http_transport/http1/transport.d | 19 ++++++++++++++----- .../response_output_stream.d | 18 ++++++++++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/source/handy_http_transport/http1/task_pool.d b/source/handy_http_transport/http1/task_pool.d index 6050cc9..92769b7 100644 --- a/source/handy_http_transport/http1/task_pool.d +++ b/source/handy_http_transport/http1/task_pool.d @@ -56,7 +56,13 @@ unittest { HttpRequestHandler handler = HttpRequestHandler.of( (ref ServerHttpRequest request, ref ServerHttpResponse response) { response.status = HttpStatus.OK; - response.writeBodyString("Testing"); }); testHttp1Transport(new TaskPoolHttp1Transport(handler)); + + HttpRequestHandler handler2 = HttpRequestHandler.of( + (ref ServerHttpRequest request, ref ServerHttpResponse response) { + response.status = HttpStatus.OK; + response.writeBodyString("Testing"); + }); + testHttp1Transport(new TaskPoolHttp1Transport(handler2)); } diff --git a/source/handy_http_transport/http1/transport.d b/source/handy_http_transport/http1/transport.d index 79d25ce..e57edde 100644 --- a/source/handy_http_transport/http1/transport.d +++ b/source/handy_http_transport/http1/transport.d @@ -87,7 +87,7 @@ version(unittest) { string httpResponseContent = cast(string) buffer[0 .. totalBytesReceived]; string[] parts = httpResponseContent.split("\r\n\r\n"); - assert(parts.length > 0, "HTTP 1.1 response is missing required status and headers section."); + assert(parts.length > 0, "HTTP 1.1 response is missing required status and headers section:\n\n" ~ httpResponseContent); string[] headerLines = parts[0].split("\r\n"); assert(headerLines.length > 0, "HTTP 1.1 response is missing required status line."); string statusLine = headerLines[0]; @@ -131,13 +131,22 @@ void handleClient(Socket clientSocket, HttpRequestHandler requestHandler) { scope ServerHttpRequest request = result.request; scope ServerHttpResponse response; SocketOutputStream* outputStream = new SocketOutputStream(clientSocket); - response.outputStream = outputStreamObjectFor(HttpResponseOutputStream!(SocketOutputStream*)( - outputStream, - &response - )); + HttpResponseOutputStream!(SocketOutputStream*) responseOutputStream + = HttpResponseOutputStream!(SocketOutputStream*)( + outputStream, + &response + ); + response.outputStream = outputStreamObjectFor(&responseOutputStream); try { requestHandler.handle(request, response); debugF!"%s %s -> %d %s"(request.method, request.url, response.status.code, response.status.text); + // If the response's headers aren't flushed yet, write them now. + if (!responseOutputStream.areHeadersFlushed()) { + auto writeResult = responseOutputStream.writeHeaders(); + if (writeResult.hasError) { + errorF!"Failed to write response headers: %s"(writeResult.error.message); + } + } } catch (Exception e) { error("Exception thrown while handling request.", e); } catch (Throwable t) { diff --git a/source/handy_http_transport/response_output_stream.d b/source/handy_http_transport/response_output_stream.d index ac302e4..3fee831 100644 --- a/source/handy_http_transport/response_output_stream.d +++ b/source/handy_http_transport/response_output_stream.d @@ -23,6 +23,15 @@ struct HttpResponseOutputStream(S) if (isByteOutputStream!S) { this.response = response; } + /** + * Determines if the HTTP response headers have been flushed to the + * underlying output stream. + * Returns: `true` if the headers have been flushed, `false` otherwise. + */ + bool areHeadersFlushed() const { + return headersFlushed; + } + /** * Writes the given data to the stream. If the referenced HTTP response's * status and headers haven't yet been written, they will be written first. @@ -37,7 +46,6 @@ struct HttpResponseOutputStream(S) if (isByteOutputStream!S) { auto result = writeHeaders(); if (result.hasError) return result; bytesWritten += result.count; - headersFlushed = true; } auto result = outputStream.writeToStream(buffer); if (result.hasError) return result; @@ -50,6 +58,10 @@ struct HttpResponseOutputStream(S) if (isByteOutputStream!S) { * Returns: The stream result of writing. */ StreamResult writeHeaders() { + if (headersFlushed) { + return StreamResult(0); // No need to write again. + } + headersFlushed = true; size_t idx = 0; char[6] statusCodeBuffer; // Normal HTTP codes are 3 digits, but this leaves room for extensions. writeUIntToBuffer(response.status.code, statusCodeBuffer, idx); @@ -111,13 +123,15 @@ unittest { resp.status = HttpStatus.OK; resp.headers.add("Content-Type", "text/plain"); auto httpOut = HttpResponseOutputStream!(ArrayOutputStream!ubyte*)(&os, &resp); - resp.outputStream = outputStreamObjectFor(httpOut); + resp.outputStream = outputStreamObjectFor(&httpOut); + assert(httpOut.areHeadersFlushed() == false); StreamResult r = resp.outputStream.writeToStream(cast(ubyte[]) "Hello world!"); const expectedOutput = "HTTP/1.1 200 OK\r\n" ~ "Content-Type: text/plain\r\n" ~ "\r\n" ~ "Hello world!"; + assert(httpOut.areHeadersFlushed() == true); assert(os.toArray() == expectedOutput); assert(r.hasCount); assert(r.count == os.toArray().length);