From d875f16d3395bd1da128930fb1348ba5c61ceffa Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Wed, 27 Sep 2023 15:59:51 -0400 Subject: [PATCH] Finished handy-httpd article. --- articles/api-with-handy-httpd.html | 65 ++++++ .../images/handy-httpd-filter-diagram.svg | 195 ++++++++++++++++++ .../images/handy-httpd-filter-diagram.webp | Bin 0 -> 23540 bytes 3 files changed, 260 insertions(+) create mode 100644 articles/images/handy-httpd-filter-diagram.svg create mode 100644 articles/images/handy-httpd-filter-diagram.webp diff --git a/articles/api-with-handy-httpd.html b/articles/api-with-handy-httpd.html index e1f3a74..60b5dcf 100644 --- a/articles/api-with-handy-httpd.html +++ b/articles/api-with-handy-httpd.html @@ -141,6 +141,71 @@ } +

+ You're not limited to only JSON though; users can upload URL-encoded data, or XML, or literally anything else. It's just that D's standard library provides a JSON implementation, so Handy-Httpd gives you some help with it. +

+

+ Under the hood, Handy-Httpd uses the streams library for the underlying data transfer, so if you're looking for a completely custom solution, you'll need to read from ctx.request.inputStream, which is a InputStream!ubyte. Also note that each request has a pre-allocated receiveBuffer that you can use instead of creating your own separate buffer. +

+ +
+

Adding Middleware with Filters

+

+ One of the buzz words these days in web programming is "middleware", which is just a fancy term for anything that sits between two systems and performs some limited set of functions on the data that is passed between the systems. +

+

+ In Handy-Httpd, we've provided a convenient method of adding middleware to the HTTP request handling flow with the HttpRequestFilter, FilterChain, and the FilteredRequestHandler. +

+
+ +
An illustration of how an HTTP request is processed by a FilteredRequestHandler: first it goes through all pre-request filters, then the underlying handler, and finally any post-request filters.
+
+

+ Suppose we want to authenticate requests to certain endpoints. That's a pretty straightforward task that many web frameworks deal with. In Handy-Httpd, you'd create a filter that reads a JWT token from the request's header, decodes it, and adds user info to the request context's metadata property. +

+
+

+				class TokenFilter : HttpRequestFilter {
+					void apply(ref HttpRequestContext ctx, FilterChain filterChain) {
+						Nullable!UserInfo userInfo = tryParseToken(ctx.request.getHeader("Authorization"));
+						if (userInfo.isNull) {
+							ctx.response.setStatus(HttpStatus.UNAUTHORIZED);
+							ctx.response.writeBodyString("Invalid or missing token.");
+							return; // Exit without continuing the filter chain.
+						}
+						ctx.metadata["userInfo"] = userInfo.get();
+						filterChain.doFilter(ctx);
+					}
+
+					private Nullable!UserInfo tryParseToken(string authHeader) {
+						// Actual implementation omitted for brevity.
+					}
+				}
+			
+
+

+ Then to actually use your newly-created TokenFilter to safeguard your endpoint, you'd use the FilteredRequestHandler to wrap your endpoint and set the TokenFilter as one of the pre-request filters. +

+
+

+				FilteredRequestHandler frh = new FilteredRequestHandler(
+					mySuperSecretEndpoint,
+					[new TokenFilter()]
+				);
+			
+
+

+ That's all there is to it! No runtime magic, just composing a handler that does exactly what you tell it to. +

+
+
+

Additional Remarks

+

+ While I've done my best (which admittedly isn't that good) to keep Handy-Httpd lightweight and performant, that really isn't the number 1 goal. My goal was always to build a simple HTTP server with some nice-to-have conveniences that don't require months of diligent study to use effectively. I wanted something with sufficient documentation so that Handy-Httpd can be a D programmer's first introduction to HTTP servers. +

+

+ We're now on version 7.10.4 as of writing this article, and I feel like Handy-Httpd is in a place where I can comfortably say that it's reached those goals. Thanks for reading all the way through, and if you do try out Handy-Httpd, please do let me know what you think! +

Back to Articles diff --git a/articles/images/handy-httpd-filter-diagram.svg b/articles/images/handy-httpd-filter-diagram.svg new file mode 100644 index 0000000..d7680e8 --- /dev/null +++ b/articles/images/handy-httpd-filter-diagram.svg @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + Server + + Handler + + + + + + + Pre-Request Filters + + Post-Request Filters + + + Request + + + diff --git a/articles/images/handy-httpd-filter-diagram.webp b/articles/images/handy-httpd-filter-diagram.webp new file mode 100644 index 0000000000000000000000000000000000000000..d6167be2251754985055b2f48368ed5cdf28fac0 GIT binary patch literal 23540 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 L)