Compare commits

...

5 Commits
v1.3.0 ... main

Author SHA1 Message Date
Andrew Lalis 18851ac786 Upgrade primitives 2025-07-05 22:06:56 -04:00
Andrew Lalis eb43a1be78 Added heap-allocation to allow long-lived websocket connections.
Build and Test Module / build-and-test (push) Successful in 13s Details
Build and Test Module / integration-tests (push) Successful in 18s Details
2025-07-05 14:27:56 -04:00
Andrew Lalis de96a4a45b Added basic standard HTTP 1 test.
Build and Test Module / build-and-test (push) Successful in 14s Details
Build and Test Module / integration-tests (push) Successful in 17s Details
2025-07-01 22:42:09 -04:00
Andrew Lalis 08bb1b58af Updated primitives to 1.3.0 2025-07-01 22:13:10 -04:00
Andrew Lalis 602667879f Updated readme. 2025-07-01 21:07:41 -04:00
5 changed files with 84 additions and 16 deletions

View File

@ -10,7 +10,7 @@ implemented so far.
## HTTP/1.1 ## HTTP/1.1
Use the `Http1Transport` implementation of `HttpTransport` to serve content Use the `TaskPoolHttp1Transport` implementation of `HttpTransport` to serve content
using the HTTP/1.1 protocol. See the example below: using the HTTP/1.1 protocol. See the example below:
```d ```d

View File

@ -4,8 +4,7 @@
], ],
"copyright": "Copyright © 2024, Andrew Lalis", "copyright": "Copyright © 2024, Andrew Lalis",
"dependencies": { "dependencies": {
"handy-http-primitives": "~>1.6", "handy-http-primitives": "~>1.8",
"photon": "~>0.11",
"streams": "~>3.6", "streams": "~>3.6",
"slf4d": "~>4.0" "slf4d": "~>4.0"
}, },

View File

@ -1,9 +1,7 @@
{ {
"fileVersion": 1, "fileVersion": 1,
"versions": { "versions": {
"handy-http-primitives": "1.6.0", "handy-http-primitives": "1.8.0",
"photon": "0.11.0",
"sharded-map": "2.7.0",
"slf4d": "4.1.1", "slf4d": "4.1.1",
"streams": "3.6.0" "streams": "3.6.0"
} }

View File

@ -47,3 +47,16 @@ class TaskPoolHttp1Transport : Http1Transport {
} }
} }
} }
unittest {
import slf4d.default_provider;
auto logProvider = DefaultProvider.builder().withRootLoggingLevel(Levels.DEBUG).build();
configureLoggingProvider(logProvider);
HttpRequestHandler handler = HttpRequestHandler.of(
(ref ServerHttpRequest request, ref ServerHttpResponse response) {
response.status = HttpStatus.OK;
response.writeBodyString("Testing");
});
testHttp1Transport(new TaskPoolHttp1Transport(handler));
}

View File

@ -12,7 +12,6 @@ import handy_http_primitives.address;
import streams; import streams;
import slf4d; import slf4d;
import photon;
/** /**
* Base class for HTTP/1.1 transport, where different subclasses can define * Base class for HTTP/1.1 transport, where different subclasses can define
@ -34,6 +33,7 @@ abstract class Http1Transport : HttpTransport {
} }
void start() { void start() {
infoF!"Starting Http1Transport server on port %d."(port);
atomicStore(running, true); atomicStore(running, true);
runServer(); runServer();
} }
@ -41,10 +41,67 @@ abstract class Http1Transport : HttpTransport {
protected abstract void runServer(); protected abstract void runServer();
void stop() { void stop() {
infoF!"Stopping Http1Transport server on port %d."(port);
atomicStore(running, false); atomicStore(running, false);
} }
} }
version(unittest) {
/**
* A generic test to ensure that any Http1Transport implementation behaves
* properly to start & stop, and process requests when running.
*
* It's assumed that the given transport is configured to run on localhost,
* port 8080, and return a standard 200 OK empty response to all requests.
* Params:
* transport = The transport implementation to test.
*/
void testHttp1Transport(Http1Transport transport) {
import core.thread;
import std.string;
infoF!"Testing Http1Transport implementation: %s"(transport);
Thread thread = transport.startInNewThread();
Thread.sleep(msecs(100));
Socket clientSocket1 = new TcpSocket(new InternetAddress(8080));
const requestBody = "POST /users HTTP/1.1\r\n" ~
"Host: example.com\r\n" ~
"Content-Type: text/plain\r\n" ~
"Content-Length: 13\r\n" ~
"\r\n" ~
"Hello, world!";
ptrdiff_t bytesSent = clientSocket1.send(requestBody);
assert(bytesSent == requestBody.length, "Couldn't send the full request body to the server.");
ubyte[8192] buffer;
size_t totalBytesReceived = 0;
ptrdiff_t bytesReceived;
do {
bytesReceived = clientSocket1.receive(buffer[totalBytesReceived .. $]);
if (bytesReceived == Socket.ERROR) {
assert(false, "Socket error when attempting to receive a response from the HttpTransport server.");
}
totalBytesReceived += bytesReceived;
} while (bytesReceived > 0);
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.");
string[] headerLines = parts[0].split("\r\n");
assert(headerLines.length > 0, "HTTP 1.1 response is missing required status line.");
string statusLine = headerLines[0];
string[] statusLineParts = statusLine.split(" ");
assert(statusLineParts[0] == "HTTP/1.1");
assert(statusLineParts[1] == "200");
assert(statusLineParts[2] == "OK");
info("Testing is complete. Stopping the server.");
transport.stop();
thread.join();
}
}
/** /**
* The main logic for handling an incoming request from a client. It involves * The main logic for handling an incoming request from a client. It involves
* reading bytes from the client, parsing them as an HTTP request, passing that * reading bytes from the client, parsing them as an HTTP request, passing that
@ -55,31 +112,32 @@ abstract class Http1Transport : HttpTransport {
* requestHandler = The request handler that will handle the received HTTP request. * requestHandler = The request handler that will handle the received HTTP request.
*/ */
void handleClient(Socket clientSocket, HttpRequestHandler requestHandler) { void handleClient(Socket clientSocket, HttpRequestHandler requestHandler) {
auto inputStream = SocketInputStream(clientSocket); SocketInputStream* inputStream = new SocketInputStream(clientSocket);
auto bufferedInput = bufferedInputStreamFor!(8192)(inputStream); BufferedInputStream!(SocketInputStream*, 8192)* bufferedInput
= new BufferedInputStream!(SocketInputStream*, 8192)(inputStream);
// Get remote address from the socket. // Get remote address from the socket.
import handy_http_primitives.address; import handy_http_primitives.address;
ClientAddress addr = getAddress(clientSocket); ClientAddress addr = getAddress(clientSocket);
debugF!"Handling client request from %s."(addr.toString()); traceF!"Got request from client: %s"(addr.toString());
auto result = readHttpRequest(&bufferedInput, addr); auto result = readHttpRequest(bufferedInput, addr);
debug_("Finished reading HTTP request from client.");
if (result.hasError) { if (result.hasError) {
if (result.error.code != -1) { if (result.error.code != -1) {
// Only warn if we didn't read an empty request. // Only warn if we didn't read an empty request.
warnF!"Failed to read HTTP request: %s"(result.error.message); warnF!"Failed to read request: %s"(result.error.message);
} }
inputStream.closeStream(); inputStream.closeStream();
return; return;
} }
scope ServerHttpRequest request = result.request; scope ServerHttpRequest request = result.request;
scope ServerHttpResponse response; scope ServerHttpResponse response;
SocketOutputStream outputStream = SocketOutputStream(clientSocket); SocketOutputStream* outputStream = new SocketOutputStream(clientSocket);
response.outputStream = outputStreamObjectFor(HttpResponseOutputStream!(SocketOutputStream*)( response.outputStream = outputStreamObjectFor(HttpResponseOutputStream!(SocketOutputStream*)(
&outputStream, outputStream,
&response &response
)); ));
try { try {
requestHandler.handle(request, response); requestHandler.handle(request, response);
debugF!"%s %s -> %d %s"(request.method, request.url, response.status.code, response.status.text);
} catch (Exception e) { } catch (Exception e) {
error("Exception thrown while handling request.", e); error("Exception thrown while handling request.", e);
} catch (Throwable t) { } catch (Throwable t) {