ちっちゃい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.クエリーパラメーター)
- 正しい使い方
- httpclientのtimoutとか諸々
- handlerのときのcontext消費はrequest bodyを消費しきらなきゃ…
- responseを書き込むタイミングでのエラーもエラーレスポンスを出したい

ユースケース的な
- 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といった監視ツールと連携するための公式ミドルウェア。

クライアントの使い方

💭カスタムコンテキストは嬉しいんだろうか?

実験として
実験として
- 型制約をdiに使うことをかんがえてみる
- requestのバインディングについて考えてみる
- responderパッケージ
rakudaとして
diは楽じゃないのでやりたくない

第2段階
responseの出力とかを考える

とりあえず用途を

Go 1.22から強化されたnet/http
パッケージを前提としたresponder
パッケージを作成するというアイデアは素晴らしいですね。標準パッケージの機能を活かしつつ、より便利で宣言的なレスポンス処理を実現するために、一般的なWebアプリケーションフレームワーク(WAF)が持つ以下のような機能を参考に、必要な機能を洗い出してみました。
基本的なレスポンス機能
-
多様なコンテントタイプへの対応:
-
JSON: Goの構造体を自動的にJSONにマーシャリングし、適切な
Content-Type
ヘッダー (application/json
) を設定してレスポンスを返す機能。 -
HTML:
html/template
やサードパーティのテンプレートエンジンと連携し、テンプレート名とデータを渡すだけでHTMLをレンダリングして返す機能。 - XML: 構造体をXMLにマーシャリングして返す機能。
-
プレーンテキスト: 文字列をそのまま
text/plain
として返す簡単な機能。 -
ファイル送信: 静的ファイルや、動的に生成したファイルをダウンロードさせる機能。
Content-Disposition
ヘッダーの制御も容易に行えることが望ましいです。
-
JSON: Goの構造体を自動的にJSONにマーシャリングし、適切な
-
ステータスコードとリダイレクトの簡略化:
-
http.StatusOK
(200) だけでなく、http.StatusCreated
(201) やhttp.StatusNoContent
(204) などを簡単に指定できるメソッド。 - リダイレクト先URLとステータスコード(301, 302など)を指定するだけで、リダイレクトレスポンスを生成するヘルパー関数。
-
高度な機能とWAFに期待される機能
-
エラーハンドリングとの統合:
- ハンドラー関数が
error
型を返した場合、それを解釈して適切なHTTPエラーステータスコード(例: 500 Internal Server Error)とエラーレスポンス(JSON形式など)を自動で生成する機能。 - エラーの種類によってステータスコード(例:
ErrNotFound
なら404)をマッピングできる仕組み。
- ハンドラー関数が
-
レスポンスヘッダーとCookieの容易な操作:
-
Content-Type
やContent-Length
のような基本的なヘッダーは自動で設定しつつ、カスタムヘッダーを簡単に追加・設定できる流れるようなインターフェース。 - Cookieの生成、設定(
Path
,Domain
,Max-Age
,HttpOnly
など)、削除を簡単に行うための機能。
-
-
コンテンツネゴシエーション:
- リクエストの
Accept
ヘッダーを解析し、クライアントが要求するフォーマット(JSON, XMLなど)に応じてレスポンスの形式を自動で切り替える機能。
- リクエストの
-
ストリーミングレスポンスのサポート:
- 大容量のデータをチャンクで送信したり、サーバーセントイベント(SSE)のようなリアルタイム通信を実現したりするためのストリーミング対応。
-
セキュリティ関連ヘッダーのデフォルト設定:
-
Content-Security-Policy
,X-Content-Type-Options
,X-Frame-Options
といった、セキュリティを向上させるためのHTTPヘッダーを簡単に追加、あるいはデフォルトで付与する機能。
-
設計と拡張性
-
テストの容易性:
-
net/http/httptest
パッケージと連携しやすく、生成されるレスポンスの内容(ステータスコード、ヘッダー、ボディ)を簡単に検証できるような設計。
-
-
ミドルウェアとの親和性:
- 多くのGoのWebアプリケーションで利用されるミドルウェアパターンと干渉せず、スムーズに連携できること。
これらの機能を備えることで、net/http
のシンプルさを保ちながら、Webアプリケーションフレームワークが提供するような生産性の高いレスポンス処理を実現する強力なresponder
パッケージとなるでしょう。

色々考えた結果のdesign docs

Of course. Here is a formal design document for the responder
library, based on our discussion.
responder
Package
Design Document: The Author: Gemini
Date: September 7, 2025
Status: Proposed
1. Abstract
This document proposes the design for a new Go package, responder
, intended to simplify and standardize HTTP response generation in applications using the standard net/http
library. It aims to eliminate boilerplate code by providing declarative, context-driven functions for common response types (e.g., JSON). Key state, such as the HTTP status code and a logger, is passed via the http.Request
's context.Context
, promoting a clean, middleware-friendly architecture that is highly testable.
2. Background and Motivation
Writing HTTP handlers in Go using only the standard library is powerful but often involves repetitive and error-prone boilerplate for each handler:
- Setting the
Content-Type
header. - Writing the HTTP status code.
- Encoding the response payload (e.g., marshaling a struct to JSON).
- Handling potential encoding errors.
- Logging errors consistently.
This repetition clutters handler logic, makes the code harder to read, and can lead to inconsistencies (e.g., forgetting to set a header).
The responder
package aims to solve this by encapsulating this boilerplate into simple, reusable functions. By leveraging the context.Context
, it decouples the response logic from the handler's business logic, allowing middleware to seamlessly modify response parameters like the status code or inject application-wide dependencies like a logger.
3. Goals
-
Declarative API: Handlers should be able to send a complete response with a single function call (e.g.,
responder.JSON(w, r, data)
). -
Context-Driven: All cross-cutting concerns (status code, logger) will be passed through the request
context
. This removes the need for aResponder
struct and simplifies dependency management. -
High Testability: The design must allow for easy unit testing of handlers. By using a logger interface and context injection, developers can provide mocks, such as a
*testing.T
-based logger, during tests. - Minimalism: The package should have a minimal and intuitive API, focusing on its core responsibility: writing responses.
- Safety: The package should gracefully handle client disconnects by checking the request context for cancellation before writing the response.
4. Non-Goals
-
Routing: The package will not be a router or a web framework. It is designed to complement
net/http
'sServeMux
or any third-party router. - Request Parsing/Validation: The responsibility of parsing and validating incoming request data remains with the user's handler or dedicated middleware.
- Request Body Draining: Draining and closing the request body to ensure HTTP Keep-Alive works correctly is considered an orthogonal concern, best handled by a dedicated middleware.
5. Proposed Design & API
The package will be purely functional, exporting functions and a single interface.
Logger
Interface
5.1. To decouple the package from any specific logging implementation (like slog
), a minimal logger interface will be defined.
package responder
import "context"
// Logger defines the minimal interface for a structured error logger.
// It is compatible with the slog.Logger and can be easily implemented
// by wrappers around other loggers or for testing purposes.
type Logger interface {
ErrorContext(ctx context.Context, msg string, args ...any)
}
5.2. Context Helper Functions
These functions provide a type-safe way to add and retrieve values from the request context.
package responder
import (
"context"
"net/http"
)
// WithLogger returns a new request with the provided Logger stored in its context.
// This should typically be called once by a middleware at the top level.
func WithLogger(r *http.Request, logger Logger) *http.Request {
// ... implementation using context.WithValue ...
}
// WithStatusCode returns a new request with the provided HTTP status code
// stored in its context. This can be called by any middleware or handler
// to set or override the status for the final response.
func WithStatusCode(r *http.Request, status int) *http.Request {
// ... implementation using context.WithValue ...
}
Internal, unexported functions getLogger(ctx)
and getStatusCode(ctx)
will be used to retrieve these values, providing sane defaults (nil
for logger, http.StatusOK
for status).
5.3. Responder Functions
The primary function for sending a JSON response.
package responder
import "net/http"
// JSON marshals the 'data' payload to JSON and writes it to the response.
//
// It performs the following steps:
// 1. Checks if the request context has been canceled (e.g., client disconnected).
// If so, it returns immediately to prevent "broken pipe" errors.
// 2. Retrieves the HTTP status code from the request context. If not set,
// it defaults to http.StatusOK (200).
// 3. Sets the "Content-Type" header to "application/json; charset=utf-8".
// 4. Writes the HTTP status code to the response header.
// 5. If data is not nil, it encodes the data to the response writer.
// 6. If encoding fails, it retrieves the Logger from the context. If a logger
// exists, it logs the error with contextual information.
func JSON(w http.ResponseWriter, req *http.Request, data any) {
// ... implementation ...
}
6. Usage Example
6.1. Application Usage
// main.go
package main
import (
"log/slog"
"net/http"
"os"
"responder" // This package
)
// LoggerMiddleware injects the application-wide logger into each request context.
func LoggerMiddleware(logger responder.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, responder.WithLogger(r, logger))
})
}
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
user, err := findUserByID(r.Context(), r.URL.Query().Get("id"))
if err == errNotFound {
r = responder.WithStatusCode(r, http.StatusNotFound)
responder.JSON(w, r, map[string]string{"error": "user not found"})
return
}
if err != nil {
r = responder.WithStatusCode(r, http.StatusInternalServerError)
// The error will be logged automatically by the JSON function if encoding fails,
// but here we log the application error itself.
logger, _ := r.Context().Value(loggerKey).(responder.Logger)
if logger != nil {
logger.ErrorContext(r.Context(), "failed to find user", "error", err)
}
responder.JSON(w, r, map[string]string{"error": "internal server error"})
return
}
// On success, the default status 200 OK is used.
responder.JSON(w, r, user)
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("/api/user", getUserHandler)
// Apply middleware
var handler http.Handler = mux
handler = LoggerMiddleware(logger)(handler)
http.ListenAndServe(":8080", handler)
}```
#### 6.2. Testing Usage
```go
// handler_test.go
package main
import (
"context"
"net/http"
"net/http/httptest"
"responder"
"testing"
)
// testLogger adapts *testing.T to the responder.Logger interface.
type testLogger struct {
t *testing.T
}
func (l *testLogger) ErrorContext(ctx context.Context, msg string, args ...any) {
l.t.Helper()
l.t.Errorf("Error Log: %s | Args: %v", msg, args)
}
func TestGetUserHandler_NotFound(t *testing.T) {
// Arrange
req := httptest.NewRequest(http.MethodGet, "/api/user?id=nonexistent", nil)
rr := httptest.NewRecorder()
// Inject test logger
logger := &testLogger{t: t}
req = responder.WithLogger(req, logger)
// Act
getUserHandler(rr, req)
// Assert
if status := rr.Code; status != http.StatusNotFound {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound)
}
expectedBody := `{"error":"user not found"}` + "\n"
if rr.Body.String() != expectedBody {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expectedBody)
}
}
7. Extensibility
This design is easily extensible. Other responder functions can be added following the same pattern:
responder.HTML(w, r, templateName, data)
responder.XML(w, r, data)
responder.File(w, r, filePath)
responder.NoContent(w, r)
Each new function would respect the status code and logger set in the context, ensuring a consistent and predictable API.

ミドルウェアとかどうなんだろ?

はい、承知いたしました。
設計したresponder
ライブラリと組み合わせて利用することで、GoのWebアプリケーションをより堅牢かつ機能的にするために有用だと思われるミドルウェアを、責務ごとに分類して箇条書きで列挙します。
1. 基盤・インフラストラクチャ層
-
ロガー注入ミドルウェア (
LoggerMiddleware
)- リクエストの
context
にアプリケーション全体で共有するslog.Logger
インスタンスを注入します。responder
がログを出力するために必須となります。
- リクエストの
-
リカバリーミドルウェア (
RecoverMiddleware
)- ハンドラ内で発生した
panic
を捕捉(recover)します。サーバー全体のクラッシュを防ぎ、500 Internal Server Error
レスポンスを返しつつ、パニック情報をログに出力します。
- ハンドラ内で発生した
-
リクエストIDミドルウェア (
RequestIDMiddleware
)- 各リクエストに一意のID(例: UUID)を付与します。そのIDを
context
に格納し、レスポンスヘッダー(例:X-Request-ID
)に含めます。ログに出力することで、リクエストの追跡が容易になります。
- 各リクエストに一意のID(例: UUID)を付与します。そのIDを
-
リクエストボディ消費ミドルウェア (
BodyDrainMiddleware
)- ハンドラの処理完了後、リクエストボディを最後まで読み飛ばして
Close()
します。HTTP/1.1のKeep-Alive接続を正しく維持し、コネクションの再利用を安定させるために重要です。
- ハンドラの処理完了後、リクエストボディを最後まで読み飛ばして
2. オブザーバビリティ(可観測性)層
-
アクセスログミドルウェア (
AccessLogMiddleware
)- リクエストを受け取ってからレスポンスを返すまでの情報をログに出力します。メソッド、パス、ステータスコード、処理時間、レスポンスサイズなど、リクエストの概要を記録します。
-
メトリクスミドルウェア (
MetricsMiddleware
)- リクエスト数、レイテンシ(処理時間)、レスポンスのステータスコード別のカウンタなどを計測し、Prometheusなどのモニタリングシステムに公開します。アプリケーションのパフォーマンス監視に不可欠です。
3. セキュリティ層
-
認証ミドルウェア (
AuthenticationMiddleware
)-
Authorization
ヘッダーなどからトークン(例: JWT)を検証し、リクエストが正当なユーザーからのものであることを確認します。検証後、ユーザーIDなどの情報をcontext
に格納して後続のハンドラに渡します。
-
-
CORSミドルウェア (
CORSMiddleware
)- ブラウザからのクロスオリジンリクエストを制御します。
OPTIONS
メソッド(プリフライトリクエスト)に応答し、Access-Control-Allow-Origin
などの適切なCORS関連ヘッダーをレスポンスに付与します。
- ブラウザからのクロスオリジンリクエストを制御します。
-
セキュリティヘッダーミドルウェア (
SecurityHeadersMiddleware
)-
X-Content-Type-Options: nosniff
、X-Frame-Options: DENY
、Content-Security-Policy
など、セキュリティを強化するためのHTTPレスポンスヘッダーをデフォルトで付与します。
-
4. アプリケーション・ユーティリティ層
-
タイムアウトミドルウェア (
TimeoutMiddleware
)- リクエスト全体の処理時間を制限します。設定した時間を超えた場合、
context
をキャンセルし、503 Service Unavailable
などのタイムアウトエラーを返します。
- リクエスト全体の処理時間を制限します。設定した時間を超えた場合、
-
レスポンス圧縮ミドルウェア (
CompressMiddleware
)- クライアントが対応している場合(
Accept-Encoding
ヘッダーにgzip
などを含む)、レスポンスボディをGzipなどで圧縮して転送量を削減します。
- クライアントが対応している場合(
-
コンテントタイプ検証ミドルウェア (
ContentTypeMiddleware
)-
POST
やPUT
リクエストなど、ボディを持つリクエストに対してContent-Type
ヘッダーを検証します。APIがapplication/json
のみを受け付ける場合、それ以外のリクエストを415 Unsupported Media Type
で弾きます。
-

ミドルウェアをくっつけるのは結局全部net/http.Handlerだから問題ないのか

はい、承知いたしました。
Goのnet/http
におけるミドルウェアの適用方法は非常に柔軟で、その基本は「http.Handler
を受け取り、http.Handler
を返す関数」というパターンに集約されます。
このパターンを使い、「特定のハンドラにだけ適用する場合」と「複数のハンドラグループ全体に適用する場合」のコードの書き方を、具体的な例と共に解説します。
前提:ミドルウェアの定義
例として、シンプルで汎用的な2つのミドルウェアを定義します。これらはresponder
パッケージには依存しない、純粋なnet/http
用ミドルウェアです。
// middleware.go
// Middlewareは、ミドルウェアの標準的な型シグネチャ
type Middleware func(http.Handler) http.Handler
// RequestIDMiddleware はリクエストにIDを付与する
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ここでは単純化のため固定のID
requestID := "dummy-request-id-12345"
// レスポンスヘッダーにIDを追加
w.Header().Set("X-Request-ID", requestID)
// ここでcontextにIDを詰めて後続に渡す実装も可能
next.ServeHTTP(w, r)
})
}
// LoggingMiddleware はリクエストのパスをログに出力する
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request received for path: %s", r.URL.Path)
next.ServeHTTP(w, r)
})
}
1. 特定のハンドラだけに適用する場合
これは最も単純なケースです。ハンドラ関数をミドルウェア関数で直接「ラップ」または「ネスト」させることで実現します。
ミドルウェアは外側から順に実行されます。A(B(handler))
の場合、Aの処理 → Bの処理 → handlerの処理 の順で実行されます。
コード例
// main.go
func main() {
mux := http.NewServeMux()
// 1. ミドルウェアを適用しないハンドラ
publicHandler := func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("This is a public endpoint."))
}
mux.HandleFunc("/public", publicHandler)
// 2. 複数のミドルウェアを特定のハンドラにだけ適用
privateHandler := func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("This is a private endpoint, accessed with logging and a request ID."))
}
// LoggingMiddleware(RequestIDMiddleware(...)) のようにネストして適用する
// http.HandlerFunc()で関数を型変換するのを忘れないように注意
mux.Handle("/private", LoggingMiddleware(RequestIDMiddleware(http.HandlerFunc(privateHandler))))
log.Println("Server starting on :8080")
http.ListenAndServe(":8080", mux)
}
ポイント:
- 適用したいハンドラを、適用したいミドルウェア関数で包んでいくだけです。
-
http.HandlerFunc
は、func(w http.ResponseWriter, r *http.Request)
というシグネチャの関数をhttp.Handler
インターフェースに適合させるための型変換です。
2. 複数のハンドラグループ全体に適用する場合
/api/v1/
以下すべてのエンドポイントに共通のミドルウェアを適用したい、といったケースです。これには、**サブ ルーター(sub-router)**のパターンを使うのが最もクリーンで管理しやすい方法です。
- グループ用の新しい
http.ServeMux
(サブルーター)を作成します。 - そのサブルーターに、グループ内のハンドラを登録します。
- グループ全体に適用したいミドルウェアで、そのサブルーター全体をラップします。
- メインのルーターに、ラップされたサブルーターを特定のパスプレフィックスで登録します。
コード例
// main.go
// Chainは複数のミドルウェアをまとめて適用するためのヘルパー関数
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
func main() {
// === API用のサブルーターとハンドラを定義 ===
apiMux := http.NewServeMux()
usersHandler := func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("API endpoint for users"))
}
productsHandler := func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("API endpoint for products"))
}
apiMux.HandleFunc("/users", usersHandler)
apiMux.HandleFunc("/products", productsHandler)
// === メインのルーターを定義 ===
mainMux := http.NewServeMux()
// ルートパス用のハンドラ
mainMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Main page"))
})
// APIグループに適用するミドルウェアのリスト
apiMiddlewares := []Middleware{
LoggingMiddleware,
RequestIDMiddleware,
// ここにAuthMiddlewareなどを追加できる
}
// apiMux(サブルーター)全体をChainヘルパーでラップ
// http.StripPrefixで "/api" というプレフィックスを削除してからapiMuxに渡す
mainMux.Handle("/api/", http.StripPrefix("/api", Chain(apiMux, apiMiddlewares...)))
log.Println("Server starting on :8080")
http.ListenAndServe(":8080", mainMux)
}
ポイント:
-
apiMux
という別のルーターを作ることで、関心を明確に分離しています。 -
Chain
のようなヘルパー関数を作ると、ミドルウェアの適用がChain(handler, mw1, mw2)
のように宣言的になり、可読性が向上します。 -
http.StripPrefix("/api", ...)
が非常に重要です。これにより、/api/users
へのリクエストが、プレフィックス/api
を取り除かれて/users
としてapiMux
に渡されるため、apiMux
側はプレフィックスを意識する必要がなくなります。
まとめ
ケース | 方法 | メリット |
---|---|---|
特定のハンドラ | ハンドラ関数を直接ミドルウェアでラップする | シンプルで直感的 |
複数のハンドラグループ | サブルーターパターンを利用する | 拡張性が高く、関心が分離され、コードがクリーンに保たれる |