hah

package module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 23, 2026 License: MIT Imports: 4 Imported by: 0

README

hah

Go Reference CI Codecov

hah 是一个面向 net/http 的 JSON API 边界层,专注于把请求绑定、输入治理和响应写回收敛成一套稳定、克制、可组合的接口。

它只处理 HTTP 边界。它不接管 router,不定义新的 handler 协议,也不包装整个 HTTP 生命周期。你可以把它接到 ServeMuxchi 或现有中间件栈后面。

为什么用 hah

  • 面向 net/http 设计,保留标准 handler 和 router 控制权
  • hah.Path(...) / hah.Query(...) 作为默认请求侧 API,直接读取 path/query 参数
  • 支持把 query、body 绑定到 DTO,再由调用方显式做后续校验
  • 把常见请求字段错误收敛为稳定的公开 HTTP 错误
  • 内置统一 JSON envelope 成功响应与错误响应
  • 根包提供默认且完整的公开 HTTP 边界
  • 适合渐进接入现有服务,不要求整体迁移

不负责什么

  • 选择或内建 validation library
  • auth / challenge / rate limit / CORS / redirect
  • router 级 404/405
  • panic recover,包括调用方自定义 MarshalJSONError 实现触发的 panic
  • tracing / access log / metrics 基础设施
  • websocket / streaming runtime

安装

环境要求:

  • Go 版本以 go.mod 为准,当前为 1.25.9

安装模块:

go get github.com/kanata996/hah@latest

导入根包:

import "github.com/kanata996/hah"

快速示例

package main

import (
	"log"
	"net/http"
	"strings"

	"github.com/kanata996/hah"
)

type createAccountRequest struct {
	Name string `json:"name"`
}

func validateCreateAccountRequest(req *createAccountRequest) error {
	req.Name = strings.TrimSpace(req.Name)
	if req.Name == "" {
		return hah.InvalidRequest(hah.FieldError{
			Field: "name",
			In:    hah.InBody,
			Code:  hah.CodeRequired,
		})
	}
	return nil
}

func writeError(w http.ResponseWriter, err error) {
	if writeErr := hah.WriteError(w, err); writeErr != nil {
		log.Printf("write error response failed: %v", writeErr)
	}
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("POST /orgs/{org_id}/accounts", func(w http.ResponseWriter, r *http.Request) {
		orgID, err := hah.Path(r, "org_id").String().Required().Get()
		if err != nil {
			writeError(w, err)
			return
		}

		var req createAccountRequest
		if err := hah.BindBody(r, &req); err != nil {
			writeError(w, err)
			return
		}
		if err := validateCreateAccountRequest(&req); err != nil {
			writeError(w, err)
			return
		}

		if err := hah.Created(w, map[string]any{
			"id":     "acct_123",
			"org_id": orgID,
			"name":   req.Name,
		}); err != nil {
			log.Printf("write success response failed: %v", err)
		}
	})

	log.Fatal(http.ListenAndServe(":8080", mux))
}

这个例子展示的是 hah 的默认使用路径:读取 path 参数,绑定 body,显式补充输入规则,然后统一写回默认 JSON envelope。 hah 是默认且唯一推荐的公开入口。只有在你明确拆分 request-side 能力、或直接依赖输入层契约时,才退到 reqx

上手路径

主流程通常是以下几步:

  • hah.Path(...) / hah.Query(...) 读取单字段 path/query 参数
  • hah.BindQuery(...) / hah.BindBody(...) 绑定 DTO
  • hah.InvalidRequest(...) 补充显式请求规则
  • hah.WriteError(...) / hah.OK(...) / hah.Accepted(...) / hah.Created(...) / hah.NoContent(...) 写回响应
  • 若要自定义响应结构,成功路径直接自行组 DTO 并 hah.JSON(...),错误路径优先用 hah.NormalizeError(...) 复用稳定错误语义;完整示例见 RESPONSES.md

公开 API 速览

请求输入:

  • hah.Path(...) 面向 path segment 中的资源标识,只保留 String()UUID()Int()Int64()Uint()Uint64()
  • hah.Query(...) 承载更宽的参数语义,除了常见标量外,还支持 Bool()Float64()Duration()Time()UnixTime();其中 Time() 要求严格 RFC3339 时间戳语法,UnixTime() 只接受恰好 10 个十进制数字
  • hah.Query(...).String() / Int() / UUID() 等单值 helper 在重复 query key 上会返回稳定 invalid_request
  • hah.Query(...).Values() 可直接读取同名 query 参数的全部解析后值;如果你需要批量结构化解码,优先用 hah.BindQuery(...)
  • hah.Query(...) 是单参数 helper,只对当前显式读取的 key 负责;其他未消费 query 参数默认不会触发额外报错

DTO binding 与显式规则:

  • hah.BindQuery(...) 只负责 query -> DTO 的映射,不内建请求级校验
  • hah.BindBody(...) 只负责 JSON body -> DTO 的解码,不替代业务层或 validation library 的规则
  • hah.InvalidRequest(...) 负责把显式输入错误收敛到稳定的 invalid_request
  • header 通常直接使用标准库 r.Header.Get(...) / r.Header.Values(...)

错误与响应:

  • hah.FieldErrorhah.HTTPErrorhah.NewHTTPError(...)hah.NewHTTPErrorWithCause(...) 是根包暴露的公共错误模型入口
  • nil*hah.HTTPError receiver 不属于支持的公开使用方式;调用方应只在持有真实错误值时再调用其方法
  • hah.BadRequest(...)hah.NotFound(...)hah.Conflict(...)hah.UnprocessableEntity(...)hah.InternalServer(...) 等快捷构造器适合在已明确公开错误语义的更深层直接返回
  • hah.NormalizeError(...) 会把任意错误收敛成稳定的公开 HTTPError;适合自定义错误响应结构时复用错误语义
  • hah.WriteError(...) 会把任意错误收敛成稳定的公开错误对象,再写成统一 JSON error envelope
  • hah.OK(...) / hah.Accepted(...) / hah.Created(...) 会写默认成功 envelope:顶层固定 code = 0message = "success",业务数据放在可选 data
  • hah.NoContent(...) 会显式写 204 No Content,同时清理冲突的 Content-Type / Content-Length
  • hah.WriteError(...) 会写默认错误 envelope:顶层 code 是五位业务错误码,未显式传入时按 status * 100 生成;顶层 message 直接来自 hah.HTTPError.Detail()
  • 默认错误 envelope 的 error 对象固定包含稳定的 reason;如果有 field errors,再按顺序附带 details
  • hah.HTTPError 未显式提供 detail,共享错误模型会基于公开 reason 生成默认短语,例如 internal_error -> "internal error"
  • hah.JSON(...) 仍是调用方指定状态码与原始 JSON body 的 escape hatch,不参与默认 envelope 协议
  • hah.WriteError(...) 的返回值表示响应边界自身异常,例如响应写出失败;生产代码通常至少要记录这个错误

响应输出选型:

  • 默认成功/失败协议直接用 hah.OK(...) / hah.Accepted(...) / hah.Created(...) / hah.NoContent(...) / hah.WriteError(...)
  • 自定义成功 JSON body 时,调用方自行组 DTO 后用 hah.JSON(...) 写出
  • 自定义错误 JSON body 时,优先先用 hah.NormalizeError(...) 复用稳定错误语义,再映射到自己的 DTO 并用 hah.JSON(...) 写出
  • 只要不需要改变 JSON body 结构,继续使用默认响应 helper 通常更简单;完整响应侧指南见 RESPONSES.md

默认错误 envelope 形状类似:

{
  "code": 42200,
  "message": "request contains invalid fields",
  "error": {
    "reason": "invalid_request",
    "details": [
      {
        "field": "name",
        "in": "body",
        "code": "required",
        "detail": "is required"
      }
    ]
  }
}

公开契约要点

hah 对公开行为的约束重点在输入与响应边界,而不是 handler 框架本身。

请求输入的关键边界:

  • hah.BindQuery(...) 的目标必须是 *struct*map[string]string
  • 对于 struct,只有显式 query tag 的顶层字段会参与绑定,其他字段保持原值;BindQuery(...) 不展开嵌套 DTO
  • hah.Query(...).Time() 以及 BindQuery(...) 中的 time.Time / *time.Time 字段都要求严格 RFC3339 时间戳语法,且时区 offset 必须合法
  • hah.BindQuery(...) 默认忽略未知 query key;同名 query key 只要出现多个值就返回稳定 400 bad_request
  • malformed raw query 返回稳定 400 bad_request 且不修改 target;DTO 或 tag 形状非法时,先返回普通错误且不修改 target
  • hah.BindQuery(...) 的严格度高于 hah.Query(...):它是整条 query source 的批量绑定入口,因此需要对 raw query 合法性和参与绑定字段的可解码性负责
  • hah.BindBody(...) 公开只支持非 nil、且根 DTO 不自定义 UnmarshalJSON*struct target
  • 非空 body 只接受且只接受一个主媒体类型为 application/jsonContent-Type
  • 零字节 body 不要求 Content-Type 为 JSON
  • body 超过 1 MiB 返回稳定 request_too_large
  • 非空 body 必须恰好构成一个以 object 为顶层值的 JSON 文档,未知字段默认拒绝
  • struct 字段解码直接跟随标准库 encoding/json;像 json.RawMessage、字段级自定义 UnmarshalJSON / UnmarshalText 类型默认允许
  • 绑定先解到临时值,成功后才一次性提交,因此失败不会污染 target
  • 同名 JSON object key 跟随标准库 encoding/json 语义,后值覆盖前值
  • 零字节 body 对 BindBody(...) 是 no-op;仅空白字符 body 和顶层 nullBindBody(...) 视为 invalid_json

响应边界的关键约束:

  • WriteError(...) 只负责错误标准化与响应写回,不内建独立错误日志
  • 如果你需要统一的日志或指标策略,在调用方基于原始 error 和业务上下文自行处理
  • 默认带 body 的成功协议提供 OK(...) / Accepted(...) / Created(...);无 payload 成功也允许继续返回 envelope
  • 只有显式调用 NoContent(...) 时,才会写 204 No Content 且不返回响应体
  • NoContent(...) 不适合自定义 envelope;需要响应体时应使用 200 / 201 / 202hah.JSON(...) 或默认成功 helper
  • 默认错误协议不输出 error.title / error.detail / error.code;稳定错误类型统一看 error.reason
  • WriteError(w, err) 的默认顶层错误码固定按 status * 100 生成;WriteError(w, err, code) 只接受单个五位正整数业务码
  • WriteError(...) 会优先选择错误链中第一个可见的公共 HTTPError;若不存在,再按 context canceleddeadline exceededinternal error 兜底
  • NormalizeError(nil) 返回 nil;自定义错误响应结构时,可稳定使用归一化后的 Status()Code()Detail()Errors()
  • HEAD 场景沿用 net/http 默认语义:handler 正常写回,对外是否发送响应体由底层决定
  • 调用方应在开始写出响应前调用 WriteError(...)

示例:

accountID, err := hah.Path(r, "account_id").UUID().Required().Get()
tags, err := hah.Query(r, "tag").Values().Get()

包边界

对外主要分成两个包:

  • hah:默认公开 HTTP 边界,也是唯一推荐的主入口;聚合常用 request helper、绑定、显式请求规则、公共错误模型入口与响应写回入口
  • reqx:较低层的请求侧公开包,负责 Path / QueryBindQuery / BindBodyInvalidRequest 以及 request-side field error 规范化 只有当你直接依赖输入层时,才把 FieldError / Code* / In* 等 request-side 契约视为 reqx 的公开面;常规 handler 路径仍优先用 hah

实现层还包含 internal/errx(共享 HTTP 错误模型)与 internal/resp(默认 JSON success/error envelope 写回),但它们都不属于公开 API。

深入文档

请求输入:

  • REQUESTS.md:以 hah.xx 为主路径的 request helper、binding、显式 post-bind validation 模式和常见组合方式

响应输出:

  • RESPONSES.md:默认响应 helper、自定义 JSON 响应结构、NormalizeError(...) 复用公开错误语义的推荐路径

公开 API:

示例与命令

示例目录:

Documentation

Overview

Package hah 提供默认的根包 HTTP 边界入口,聚合请求输入、公共错误模型与 JSON 响应写回。

适合在大多数 handler 中直接使用:

  • 核心 request helper:Path、Query
  • 明确分离的 DTO 绑定入口:BindQuery、BindBody
  • 常见请求字段错误与公共错误模型:InvalidRequest、FieldError、HTTPError
  • 常用 JSON 成功响应辅助
  • 统一错误响应写回

当前项目里,hah 是默认入口;多数调用方不需要直接 import reqx。 只有当你明确在拆分 request-side 组件、并需要更低层的输入侧公开面时, 才退到 reqx.xx。

公开 API:

  • request helper:Path、Query
  • 绑定入口:BindQuery、BindBody
  • 请求级规则 helper:InvalidRequest
  • 公共错误模型:FieldError、HTTPError、NewHTTPError、NewHTTPErrorWithCause
  • 常用错误快捷构造:BadRequest、Unauthorized、Forbidden、NotFound、 MethodNotAllowed、Conflict、UnprocessableEntity、TooManyRequests、 InternalServer
  • 错误归一化入口:NormalizeError
  • 公开 field error 常量:Code*、In*
  • 错误响应入口:WriteError
  • 成功响应入口:JSON、OK、Accepted、Created、NoContent

当前根包是默认且唯一推荐的公开入口;错误与响应边界固定收敛在这里。 reqx 仍然是公开包,但定位为较低层的 request-side 原生面,而不是并列主入口。

Index

Constants

View Source
const (
	CodeInvalid  = reqx.CodeInvalid
	CodeRequired = reqx.CodeRequired
	CodeUnknown  = reqx.CodeUnknown
	CodeType     = reqx.CodeType
	CodeMultiple = reqx.CodeMultiple
)
View Source
const (
	InBody   = reqx.InBody
	InQuery  = reqx.InQuery
	InPath   = reqx.InPath
	InHeader = reqx.InHeader
)

Variables

This section is empty.

Functions

func Accepted added in v0.8.4

func Accepted(w http.ResponseWriter, data any) error

Accepted 写回 202 成功响应。

func BindBody added in v0.2.0

func BindBody(r *http.Request, target any) error

BindBody 只从请求 body 绑定数据。

绑定会先解码到临时值,成功后再一次性提交到 target。

func BindQuery added in v0.5.0

func BindQuery(r *http.Request, target any) error

BindQuery 只从 query 参数绑定数据。

func Created added in v0.2.0

func Created(w http.ResponseWriter, data any) error

Created 写回 201 成功响应。

func InvalidRequest added in v0.1.1

func InvalidRequest(fieldErrors ...FieldError) error

InvalidRequest 生成统一的 invalid_request 错误包络。

func JSON added in v0.2.0

func JSON(w http.ResponseWriter, status int, data any) error

JSON 写回 JSON 响应。

func NoContent added in v0.2.0

func NoContent(w http.ResponseWriter) error

NoContent 写回 204 无响应体成功响应。

func OK added in v0.2.0

func OK(w http.ResponseWriter, data any) error

OK 写回 200 成功响应。

func Path added in v0.4.1

func Path(r *http.Request, name string) *reqx.PathParam

Path 创建 path 单参数读取与校验 builder。

func Query added in v0.4.1

func Query(r *http.Request, name string) *reqx.QueryParam

Query 创建 query 单参数读取与校验 builder。

func WriteError

func WriteError(w http.ResponseWriter, err error, topCode ...int) error

WriteError 按统一错误对象写回响应。

Types

type FieldError added in v0.8.3

type FieldError = reqx.FieldError

FieldError 描述单个公开字段错误。

type HTTPError

type HTTPError = errx.HTTPError

HTTPError 表示 HTTP 边界上的公共错误。

func BadRequest

func BadRequest(code, detail string) *HTTPError

BadRequest 构造 400 Bad Request 公共错误。

func Conflict

func Conflict(code, detail string) *HTTPError

Conflict 构造 409 Conflict 公共错误。

func Forbidden

func Forbidden(code, detail string) *HTTPError

Forbidden 构造 403 Forbidden 公共错误。

func InternalServer added in v0.8.3

func InternalServer(code, detail string) *HTTPError

InternalServer 构造 500 Internal Server Error 公共错误。

func MethodNotAllowed

func MethodNotAllowed(code, detail string) *HTTPError

MethodNotAllowed 构造 405 Method Not Allowed 公共错误。

func NewHTTPError

func NewHTTPError(status int, code, detail string) *HTTPError

NewHTTPError 构造一个不带底层 cause 的公共 HTTP 错误。

func NewHTTPErrorWithCause added in v0.5.2

func NewHTTPErrorWithCause(status int, code, detail string, cause error) *HTTPError

NewHTTPErrorWithCause 基于给定 cause 构造公共 HTTP 错误。

func NormalizeError added in v0.9.0

func NormalizeError(err error) *HTTPError

NormalizeError 收敛任意错误链,返回可公开暴露的 HTTPError。

对 nil 输入返回 nil。

func NotFound

func NotFound(code, detail string) *HTTPError

NotFound 构造 404 Not Found 公共错误。

func TooManyRequests

func TooManyRequests(code, detail string) *HTTPError

TooManyRequests 构造 429 Too Many Requests 公共错误。

func Unauthorized

func Unauthorized(code, detail string) *HTTPError

Unauthorized 构造 401 Unauthorized 公共错误。

func UnprocessableEntity

func UnprocessableEntity(code, detail string) *HTTPError

UnprocessableEntity 构造 422 Unprocessable Entity 公共错误。

Directories

Path Synopsis
internal
errx
Package errx 提供共享 HTTP 错误模型的内部实现。
Package errx 提供共享 HTTP 错误模型的内部实现。
resp
Package resp 为基于 net/http 的 JSON API 提供响应侧辅助能力的内部实现。
Package resp 为基于 net/http 的 JSON API 提供响应侧辅助能力的内部实现。
Package reqx 为基于 net/http 的 JSON API 提供输入侧能力。
Package reqx 为基于 net/http 的 JSON API 提供输入侧能力。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL