Open27

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

podhmopodhmo

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

名前はrakudaとかにする

  • 駱駝に乗りたい(手軽に動きたい)
  • 楽だ
podhmopodhmo

たぶん雰囲気的にpythonのpyramidの影響を受ける。でもそこまで本格的にはしない。登録を順序を気にしたくない程度で拡張性を持たせたディレクティブ(pyramid用語)とかは作るつもりはない。

https://trypyramid.com/

podhmopodhmo

第一段階

とりあえずgo-chiを参考にするけれど登録順序を気にしたくない。なのでクロージャとして保持してソートしてから登録することにする。

このコミット的な処理の呼び忘れを防ぐためにビルダーとルーターは分ける。

https://aistudio.google.com/app/prompts?state={"ids":["1TlE_kFl3zbaYfhWkwC-QEVNtZrDUwHBC"],"action":"open","userId":"108405443477417806091","resourceKeys":{}}&usp=sharing

podhmopodhmo

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

2.1. Unambiguous Separation of Concerns: Builder vs. http.Handler

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 the http.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. The Builder.Build() method returns a value of this type. It is the final, immutable, and executable handler that is passed to http.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) or Group(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.

  1. Declaration Phase: When you call methods like Use() or Get() on a Builder, 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.

  2. 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 final http.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

3.1. Type Builder

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.

Configuration Methods

  • WithLogger(logger *slog.Logger) *Builder

    • Attaches a slog.Logger for debugging and internal logging. Returns the builder for chaining.
  • 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 new Builder instance that operates on this new node.
  • Route(pattern string, fn func(b *Builder))

    • Creates a new child node with a URL path prefix. The function fn receives a new Builder instance for this sub-route.

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}".
  • Get(pattern string, handler http.Handler)

    • A convenience wrapper for Handle("GET "+pattern, handler).
  • 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 like GetFunc are intentionally omitted for a leaner API. Users should wrap functions with http.HandlerFunc.)

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, the Builder 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.

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 after Build() has been called will result in an error on subsequent method calls.
  • There will be no panics for any configuration-related issues.
podhmopodhmo

登録がうまくいってるか確認するためのそして全体を辿って登録状況を確認できるproutes的なコマンドが欲しい

😔 あと仕様と呼ぶと外部向けのインターフェイスが出力され会話した内部的な意思決定が消える。design docs/とか言えば良いのかな。

podhmopodhmo

Design Document: rakuda HTTP Router

  • 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

3.1. Core Components: Builder and http.Handler

The entire design hinges on the strict separation of two types:

  • rakuda.Builder: This is the sole object responsible for configuration. It exposes methods like Get, Post, Use, Route, etc. Critically, it does not implement the http.Handler interface. Its only responsibility is to build and maintain an internal configuration tree.

  • http.Handler: This is the standard library interface. The Builder.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 a node in this tree.
  • NewBuilder() creates the root node.
  • Route(pattern, fn) and Group(fn) create a new child node under the current node. The function fn receives a new Builder 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:

  1. Traversal: It performs a Depth-First Search (DFS) traversal of the entire configuration tree, starting from the root.
  2. Context Accumulation: As it traverses, it keeps track of the accumulated path prefix (e.g., /api + /v1) and the inherited middleware chain.
  3. 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 new http.ServeMux instance, using the full, concatenated path pattern (e.g., GET /api/v1/users/{id}).
  4. Finalization: Once the traversal is complete, the populated http.ServeMux (which is itself an http.Handler) is returned. The Builder 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 time ServeHTTP is called, likely using a sync.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 implement http.Handler and expose configuration methods. A Build() 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 (if ServeHTTP 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.
podhmopodhmo

細々とした事項

初回に作るときのほそぼそとした事項。なるべく依存を増やさない方針にしたい。

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 ])
podhmopodhmo

TODO

  • 設定の読み込みどうする?
  • 実際のハンドラーの定義どうする?
  • リクエストのバインドどうする?(e.g.クエリーパラメーター)
  • 正しい使い方
    • httpclientのtimoutとか諸々
    • handlerのときのcontext消費はrequest bodyを消費しきらなきゃ…
    • responseを書き込むタイミングでのエラーもエラーレスポンスを出したい
podhmopodhmo

ユースケース的な

  • json response
  • redirect
  • error response (json)
podhmopodhmo

雑に聞く

承知いたしました。
現在のrakudaの設計は、安全で堅牢なHTTPルーターとしては優れていますが、Webアプリケーションを効率的に開発するための**Web Application Framework (WAF)**として見た場合、多くの便利な機能が不足しています。

以下に、WAFとして不足している機能をユースケースごとに列挙します。

ユースケース: リクエストデータの処理と検証

HTTPリクエストからデータ(JSON、フォーム、クエリパラメータ等)を安全かつ簡単に取り出すための機能です。

  • データバインディング: リクエストボディのJSONやフォームの内容を、自動的にGoのstructにマッピングする機能 (c.Bind(&userStruct))。これにより、手動でのJSONデコードやフォーム値のパースが不要になります。
  • 入力バリデーション: バインディングしたstructのフィールドに対して、「必須項目」「メールアドレス形式」「最小/最大値」などの検証ルールを宣言的に適用し、違反した場合にエラーを返す機能。
  • 型安全なパスパラメータ/クエリパラメータ取得: r.PathValue("id") は文字列を返しますが、これをintuuid.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-PolicyX-XSS-Protection といった、セキュリティ関連のHTTPヘッダを簡単に追加するためのミドルウェア。

ユースケース: 可観測性 (Observability)

アプリケーションの動作を監視し、デバッグを容易にするための機能です。

  • リクエストスコープのロガー: リクエストIDなど、リクエストに紐づく情報を含んだslog.Loggerをコンテキスト経由で利用できるようにする機能。
  • メトリクス/トレーシング連携: PrometheusやOpenTelemetryといった監視ツールと連携するための公式ミドルウェア。
podhmopodhmo

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

podhmopodhmo

実験として

実験として

  • 型制約をdiに使うことをかんがえてみる
  • requestのバインディングについて考えてみる
  • responderパッケージ

rakudaとして

diは楽じゃないのでやりたくない

podhmopodhmo
podhmopodhmo

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ヘッダーの制御も容易に行えることが望ましいです。
  • ステータスコードとリダイレクトの簡略化:

    • http.StatusOK (200) だけでなく、http.StatusCreated (201) や http.StatusNoContent (204) などを簡単に指定できるメソッド。
    • リダイレクト先URLとステータスコード(301, 302など)を指定するだけで、リダイレクトレスポンスを生成するヘルパー関数。

高度な機能とWAFに期待される機能

  • エラーハンドリングとの統合:

    • ハンドラー関数がerror型を返した場合、それを解釈して適切なHTTPエラーステータスコード(例: 500 Internal Server Error)とエラーレスポンス(JSON形式など)を自動で生成する機能。
    • エラーの種類によってステータスコード(例: ErrNotFoundなら404)をマッピングできる仕組み。
  • レスポンスヘッダーとCookieの容易な操作:

    • Content-TypeContent-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パッケージとなるでしょう。

podhmopodhmo

Of course. Here is a formal design document for the responder library, based on our discussion.


Design Document: The responder Package

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:

  1. Setting the Content-Type header.
  2. Writing the HTTP status code.
  3. Encoding the response payload (e.g., marshaling a struct to JSON).
  4. Handling potential encoding errors.
  5. 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 a Responder 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's ServeMux 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.

5.1. Logger Interface

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.

podhmopodhmo

はい、承知いたしました。
設計した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)に含めます。ログに出力することで、リクエストの追跡が容易になります。
  • リクエストボディ消費ミドルウェア (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: nosniffX-Frame-Options: DENYContent-Security-Policyなど、セキュリティを強化するためのHTTPレスポンスヘッダーをデフォルトで付与します。

4. アプリケーション・ユーティリティ層

  • タイムアウトミドルウェア (TimeoutMiddleware)

    • リクエスト全体の処理時間を制限します。設定した時間を超えた場合、contextをキャンセルし、503 Service Unavailableなどのタイムアウトエラーを返します。
  • レスポンス圧縮ミドルウェア (CompressMiddleware)

    • クライアントが対応している場合(Accept-Encodingヘッダーにgzipなどを含む)、レスポンスボディをGzipなどで圧縮して転送量を削減します。
  • コンテントタイプ検証ミドルウェア (ContentTypeMiddleware)

    • POSTPUTリクエストなど、ボディを持つリクエストに対してContent-Typeヘッダーを検証します。APIがapplication/jsonのみを受け付ける場合、それ以外のリクエストを415 Unsupported Media Typeで弾きます。
podhmopodhmo

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

podhmopodhmo

はい、承知いたしました。
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)**のパターンを使うのが最もクリーンで管理しやすい方法です。

  1. グループ用の新しいhttp.ServeMux(サブルーター)を作成します。
  2. そのサブルーターに、グループ内のハンドラを登録します。
  3. グループ全体に適用したいミドルウェアで、そのサブルーター全体をラップします。
  4. メインのルーターに、ラップされたサブルーターを特定のパスプレフィックスで登録します。

コード例

// 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側はプレフィックスを意識する必要がなくなります。

まとめ

ケース 方法 メリット
特定のハンドラ ハンドラ関数を直接ミドルウェアでラップする シンプルで直感的
複数のハンドラグループ サブルーターパターンを利用する 拡張性が高く、関心が分離され、コードがクリーンに保たれる