ちっちゃいgoのWAFを考えてみる

ちっちゃいgoのWAFでも作ってみようかな。go1.22+由来のnet/httpのハンドラーをそのまま使う感じでgo-chiに雰囲気寄せる感じで(ただし登録順序とか気にしない感じで)
名前はrakudaとかにする
- 駱駝に乗りたい(手軽に動きたい)
- 楽だ

第一段階
とりあえずgo-chiを参考にするけれど登録順序を気にしたくない。なのでクロージャとして保持してソートしてから登録することにする。
このコミット的な処理の呼び忘れを防ぐためにビルダーとルーターは分ける。

Specification: rakuda HTTP Router (Revision 3)
1. Overview
rakuda
is an HTTP router for Go that enforces a clear and safe configuration lifecycle. It achieves this by providing two distinct and separate components: a Builder
for defining routes, and the standard http.Handler
as the final, executable product. This design makes it impossible to accidentally use an unconfigured or partially configured router, as such an error would be caught by the Go compiler.
2. Core Concepts
Builder
vs. http.Handler
2.1. Unambiguous Separation of Concerns: This is the foundational principle of the library.
-
rakuda.Builder
: This is the type used exclusively for configuration. It provides all the methods for defining routes and middlewares. It does not implement thehttp.Handler
interface. Its sole purpose is to accumulate routing rules in an internal tree structure. -
http.Handler
: This is the standard Go interface for handling HTTP requests. TheBuilder.Build()
method returns a value of this type. It is the final, immutable, and executable handler that is passed tohttp.ListenAndServe
.
This strict separation ensures that a developer cannot pass a Builder
to an HTTP server, thus eliminating "forgot to build" runtime errors at compile time.
2.2. Internal Mechanism: The Configuration Tree
The Builder
manages routing rules internally as a tree structure. This is the key to handling nested routes and scoped middlewares correctly.
-
Root Node:
NewBuilder()
creates the root of the tree. -
Child Nodes: Calling
Route(pattern, fn)
orGroup(fn)
creates a new child node under the current builder's node.-
Route
creates a node associated with a path prefix. -
Group
creates a node for scoping middlewares without a path prefix.
-
-
Inheritance: When the final handler is constructed during the
Build
phase, middlewares are inherited down the tree. A handler registered on a child node will be wrapped by middlewares on that child, its parent, its grandparent, and so on, all the way to the root.
2.3. Internal Mechanism: How Order-Independence is Achieved
Order-independence is achieved by deferring all processing to the Build()
phase.
-
Declaration Phase: When you call methods like
Use()
orGet()
on aBuilder
, the router does not immediately create middleware chains or register handlers. It simply adds a "middleware registration" or a "handler registration" entry to a list within the current node of the configuration tree. -
Build Phase: When
Build()
is called, it performs a traversal (e.g., Depth-First Search) of the entire configuration tree. For each node it visits, it performs the following steps:
a. It calculates the full middleware chain applicable to that node by combining middlewares from all its ancestors (parent, grandparent, etc.) with the middlewares registered directly on the node itself.
b. It then iterates through the handlers registered on that node. For each handler, it wraps it with the complete middleware chain calculated in the previous step.
c. Finally, it registers the fully-wrapped handler with the finalhttp.ServeMux
under its complete, concatenated path.
Because the process of applying middlewares to handlers only happens at the end, the original order in which you called Use()
and Get()
within a given scope is irrelevant.
3. API Specification
Builder
3.1. Type The Builder
is the entry point for all route definitions.
Factory Function
-
NewBuilder() *Builder
- Creates and returns a new, empty
Builder
instance, representing the root of the configuration tree.
- Creates and returns a new, empty
Configuration Methods
-
WithLogger(logger *slog.Logger) *Builder
- Attaches a
slog.Logger
for debugging and internal logging. Returns the builder for chaining.
- Attaches a
-
Use(middlewares ...func(http.Handler) http.Handler)
- Appends one or more middlewares to the current node in the configuration tree.
-
Group(fn func(b *Builder))
- Creates a new child node for scoping. The function
fn
receives a newBuilder
instance that operates on this new node.
- Creates a new child node for scoping. The function
-
Route(pattern string, fn func(b *Builder))
- Creates a new child node with a URL path prefix. The function
fn
receives a newBuilder
instance for this sub-route.
- Creates a new child node with a URL path prefix. The function
Handler Registration Methods
-
Handle(pattern string, handler http.Handler)
- Registers an
http.Handler
for a given pattern within the current node. The pattern must be in the format"{METHOD} {path}"
.
- Registers an
-
Get(pattern string, handler http.Handler)
- A convenience wrapper for
Handle("GET "+pattern, handler)
.
- A convenience wrapper for
-
Post(pattern string, handler http.Handler)
-
Put(pattern string, handler http.Handler)
-
Delete(pattern string, handler http.Handler)
-
Patch(pattern string, handler http.Handler)
-
Options(pattern string, handler http.Handler)
-
Head(pattern string, handler http.Handler)
- (Note:
...Func
variants likeGetFunc
are intentionally omitted for a leaner API. Users should wrap functions withhttp.HandlerFunc
.)
- (Note:
Build Method
-
Build() (http.Handler, error)
- Executes the build process as described in section 2.3.
- Returns a final, immutable
http.Handler
. - Returns an error if the configuration is invalid.
- After
Build()
is called, theBuilder
instance becomes locked. Any subsequent calls to its configuration methods will return an error.
3.2. Route Inspection Utility
-
PrintRoutes(w io.Writer, b *Builder) error
- A helper function to visualize the configuration tree of a
Builder
instance before it is built.
- A helper function to visualize the configuration tree of a
4. Example Usage Flow
func main() {
// 1. Create a new Builder instance.
b := rakuda.NewBuilder()
// 2. Configure routes and middlewares.
b.Use(RequestLoggerMiddleware)
b.Get("/", http.HandlerFunc(handleHomepage))
b.Route("/api/v1", func(api *rakuda.Builder) {
api.Use(AuthMiddleware)
api.Get("/users", http.HandlerFunc(handleListUsers))
})
// 3. (Optional) Print the routes for debugging.
rakuda.PrintRoutes(os.Stdout, b)
// 4. Build the final http.Handler from the Builder.
handler, err := b.Build()
if err != nil {
log.Fatalf("Failed to build router: %v", err)
}
// 5. Use the resulting handler.
// http.ListenAndServe(":8080", b) // <-- This will NOT compile.
http.ListenAndServe(":8080", handler) // This is correct.
}
5. Error Handling
- Configuration errors are returned exclusively by the
Build()
method. - Any attempt to modify a
Builder
afterBuild()
has been called will result in an error on subsequent method calls. - There will be no panics for any configuration-related issues.

登録がうまくいってるか確認するためのそして全体を辿って登録状況を確認できるproutes的なコマンドが欲しい
😔 あと仕様と呼ぶと外部向けのインターフェイスが出力され会話した内部的な意思決定が消える。design docs/とか言えば良いのかな。

rakuda
HTTP Router
Design Document: - Author: Gemini
- Status: Proposed
- Date: 2025-09-07
1. Abstract
This document outlines the internal design and architecture of rakuda
, a new HTTP router for Go. The primary goal of rakuda
is to provide a type-safe and predictable routing experience by enforcing a strict separation between the configuration phase and the execution phase. This is achieved through a dedicated Builder
type for route definition and the final output of a standard http.Handler
. This design eliminates a class of common runtime errors, such as using a router before its configuration is complete.
2. Goals and Motivation
The Go ecosystem has many routers, but many either allow for runtime errors due to ambiguous lifecycles or have complex APIs. The motivation for rakuda
is to address these issues with the following core goals:
- Type Safety: The router's lifecycle must be enforced by the Go compiler. It should be impossible to compile code that uses a router before it has been explicitly built.
- Predictable Lifecycle: The configuration of routes (the "what") should be completely separate from the serving of traffic (the "how"). The router's state should be immutable once built.
- Declarative, Order-Independent API: Developers should be able to declare routes and middlewares in any order without affecting the final behavior.
-
Leverage Standard Library: Maximize the use of the standard
net/http
package, including the path parameter support introduced in Go 1.22, to ensure compatibility and minimize dependencies. - Flexible Middleware Scoping: Support applying middlewares globally, to specific route groups, or to nested groups.
3. Proposed Architecture
Builder
and http.Handler
3.1. Core Components: The entire design hinges on the strict separation of two types:
-
rakuda.Builder
: This is the sole object responsible for configuration. It exposes methods likeGet
,Post
,Use
,Route
, etc. Critically, it does not implement thehttp.Handler
interface. Its only responsibility is to build and maintain an internal configuration tree. -
http.Handler
: This is the standard library interface. TheBuilder.Build()
method is the factory for this type. The returned handler is an immutable, ready-to-use component that encapsulates all the defined routing logic.
This separation forces the developer into a safe, two-step process: first, configure the Builder
; second, Build()
it into an http.Handler
to be used by the server. An attempt to pass the Builder
to http.ListenAndServe
will result in a compile-time error.
3.2. Internal Data Structure: The Configuration Tree
To support flexible middleware scoping and nested routes (/api/v1/...
), the Builder
does not store routes in a flat list. Instead, it maintains a tree of configuration nodes.
- Each
Builder
instance holds a reference to anode
in this tree. -
NewBuilder()
creates theroot
node. -
Route(pattern, fn)
andGroup(fn)
create a newchild
node under the current node. The functionfn
receives a newBuilder
instance that points to this new child node.
A simplified representation of a node
struct would be:
type node struct {
pattern string
middlewares []func(http.Handler) http.Handler
handlers []handlerRegistration
children []*node
}
This tree structure naturally maps to the hierarchical nature of RESTful API routes.
3.3. The Build Process
All the "magic" happens within the Build()
method. The method calls on the Builder
(Get
, Use
, etc.) are lightweight operations that simply populate the configuration tree. Build()
orchestrates the following process:
- Traversal: It performs a Depth-First Search (DFS) traversal of the entire configuration tree, starting from the root.
-
Context Accumulation: As it traverses, it keeps track of the accumulated path prefix (e.g.,
/api
+/v1
) and the inherited middleware chain. -
Handler Assembly: For each node it visits, it:
a. Creates the full middleware chain for that node by appending the node's own middlewares to the chain inherited from its parent.
b. Iterates through all handlers registered on that node.
c. For each handler, it applies the full middleware chain.
d. It registers the final, wrapped handler with a newhttp.ServeMux
instance, using the full, concatenated path pattern (e.g.,GET /api/v1/users/{id}
). -
Finalization: Once the traversal is complete, the populated
http.ServeMux
(which is itself anhttp.Handler
) is returned. TheBuilder
is then marked as "built" to prevent further modification.
This deferred assembly process is what enables the order-independent API. The order of Use()
and Get()
calls only matters relative to each other after the build process has categorized and applied them.
3.4. Path Parameters
rakuda
will not implement its own path parameter parsing logic. It will rely entirely on the native capabilities of net/http
introduced in Go 1.22. Handlers are expected to retrieve path parameters directly from the request object.
Example:
// Registration
b.Get("/users/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Retrieval inside the handler
userID := r.PathValue("id")
// ...
}))
4. Rejected Alternatives
Several alternative designs were considered and rejected. Understanding these trade-offs is crucial to understanding the proposed architecture.
4.1. Alternative 1: Lazy Initialization
-
Description: The router would implement
http.Handler
, but the actual building would be deferred until the first timeServeHTTP
is called, likely using async.Once
. - Reason for Rejection: This is a poor developer experience. Configuration errors (e.g., a typo in a route, an invalid pattern) would only surface at runtime when the first request hits the server, not at application startup. This violates the principle of failing fast.
4.2. Alternative 2: Combined Router/Builder Type
-
Description: A single
Router
type would both implementhttp.Handler
and expose configuration methods. ABuild()
method might exist, but its use wouldn't be enforced by the compiler. -
Reason for Rejection: This is the most common pattern, but it's prone to error. A developer could easily forget to call
Build()
and pass the unconfigured or partially configured router to the server. This would lead to runtime panics (ifServeHTTP
checks for a built state) or silent failures (404s for all routes). The proposed design solves this at compile time.
4.3. Alternative 3: Flat Slice and Sorting
-
Description: Instead of a tree, all registrations (middlewares and handlers) would be appended to a single flat slice. Each entry would have a "type" or "priority" (e.g., middleware=1, handler=2). During
Build()
, this slice would be sorted to ensure middlewares are processed before handlers. -
Reason for Rejection: This approach is too simplistic. It cannot handle scoped middlewares. For example, you cannot apply a middleware only to routes under
/api
. All middlewares would effectively become global, severely limiting the router's utility for any non-trivial application. The tree structure is essential for this functionality.

細々とした事項
初回に作るときのほそぼそとした事項。なるべく依存を増やさない方針にしたい。
scaffoldするときの命令的なものにいれるやつら
- READMEを英語で書いてください
- テストコードを追加してください
- go1.24を使ってください
- ログにはlog/slogだけを使ってください
- デバッグプリントはslogのDebugContextを使ってください
- http routerは net/http のものを使ってください
- テストコードのassertにはgo-cmpを使ってください
- テストコードのassertでは変数名としてwant,gotのペアを使ってください
- github.com/stretchr/testify は使わないでください
- github actionsのciを追加してください(pull_request で types: [ opened, synchronize, reopened ])

TODO
- 設定の読み込みどうする?
- 実際のハンドラーの定義どうする?
- リクエストのバインドどうする?(e.g.クエリーパラメーター)

ユースケース的な
- json response
- redirect
- error response (json)

雑に聞く
承知いたしました。
現在のrakuda
の設計は、安全で堅牢なHTTPルーターとしては優れていますが、Webアプリケーションを効率的に開発するための**Web Application Framework (WAF)**として見た場合、多くの便利な機能が不足しています。
以下に、WAFとして不足している機能をユースケースごとに列挙します。
ユースケース: リクエストデータの処理と検証
HTTPリクエストからデータ(JSON、フォーム、クエリパラメータ等)を安全かつ簡単に取り出すための機能です。
-
データバインディング: リクエストボディのJSONやフォームの内容を、自動的にGoのstructにマッピングする機能 (
c.Bind(&userStruct)
)。これにより、手動でのJSONデコードやフォーム値のパースが不要になります。 - 入力バリデーション: バインディングしたstructのフィールドに対して、「必須項目」「メールアドレス形式」「最小/最大値」などの検証ルールを宣言的に適用し、違反した場合にエラーを返す機能。
-
型安全なパスパラメータ/クエリパラメータ取得:
r.PathValue("id")
は文字列を返しますが、これをint
やuuid.UUID
などの型に変換し、失敗した場合にエラーをハンドリングするヘルパー機能。 - ファイルアップロードの簡略化: マルチパートフォームからのファイルアップロード処理を、より簡単に扱うためのヘルパー機能。
ユースケース: レスポンスの生成
JSON、HTML、XMLなど、様々な形式のレスポンスを簡単に生成するための機能です。
-
レスポンスヘルパー:
c.JSON(http.StatusOK, data)
やc.String(http.StatusOK, "message")
のように、ステータスコード、コンテントタイプヘッダの設定、およびボディの書き込みを一行で行うヘルパー関数。 -
HTMLテンプレートレンダリング:
html/template
などのテンプレートエンジンと統合し、c.Render("index.html", data)
のような形でHTMLをレンダリングする機能。テンプレートのキャッシュ管理なども含みます。
ユースケース: アプリケーションの状態管理と依存性注入
データベース接続プールや設定情報など、アプリケーション全体で共有するリソースを各ハンドラに安全に渡すための仕組みです。
-
カスタムコンテキスト:
http.ResponseWriter
と*http.Request
をラップし、リクエストスコープの値、パスパラメータ、レスポンスヘルパー、アプリケーションの依存関係(DB接続など)を保持する独自のContext
型。ハンドラのシグネチャはfunc(c *rakuda.Context)
のようになります。これは多くのWAFで中核的な機能です。 - 依存性注入(DI)のパターン: カスタムコンテキストを通じて、アプリケーション起動時に初期化されたサービス(DB接続プールなど)を各ハンドラから透過的に利用できるようにするための明確なパターンや仕組み。
ユースケース: エラーハンドリングの一元化
アプリケーション全体でエラーレスポンスの形式を統一し、ハンドラ内のエラー処理の記述を簡潔にするための機能です。
-
中央エラーハンドラ: ハンドラが
error
を返り値として返せるようにし、nil
でない場合に特定のエラーレスポンス(例: 500 Internal Server ErrorのJSON)を自動で生成するミドルウェア。 - パニックリカバリー: ハンドラ内で発生したパニックを捕捉し、サーバーをクラッシュさせる代わりに500エラーとして処理するミドルウェア。
ユースケース: セキュリティ対策
Webアプリケーションにおける一般的な脆弱性から保護するための標準的な機能です。
- CORSミドルウェア: クロスオリジンリソース共有 (CORS) のヘッダを柔軟に設定するためのミドルウェア。
- CSRFミドルウェア: クロスサイトリクエストフォージェリ (CSRF) からアプリケーションを保護するためのトークンベースのミドルウェア。
-
セキュリティヘッダミドルウェア:
Content-Security-Policy
やX-XSS-Protection
といった、セキュリティ関連のHTTPヘッダを簡単に追加するためのミドルウェア。
ユースケース: 可観測性 (Observability)
アプリケーションの動作を監視し、デバッグを容易にするための機能です。
-
リクエストスコープのロガー: リクエストIDなど、リクエストに紐づく情報を含んだ
slog.Logger
をコンテキスト経由で利用できるようにする機能。 - メトリクス/トレーシング連携: PrometheusやOpenTelemetryといった監視ツールと連携するための公式ミドルウェア。