v2.0.0: error-channel APIs, correctness fixes, and server hardening

BREAKING CHANGES:
- Guard interface: Guard(ctx Ctx) error; returning error short-circuits
  (subsequent guards and the method no longer execute)
- Wired[T]() (*T, error); New() may return error; failed wiring is sticky
- BindService/BindGuard/BindRoute return error; routes accept guards
  (guards receive merged query/form params as Ctx.State)
- registration panics after Start; Ctx.Ip honors X-Forwarded-For/X-Real-IP
- internal errors sanitized to fixed client messages

FIXES:
- int64 precision loss: /cell data decoded from raw JSON bytes and
  responses serialized with json.Number (no float64 round-trip)
- enum values range-checked before uint8 conversion (256 no longer
  truncates to 0 and slips through)
- logger: files failing name parsing are no longer deleted; log channel
  never blocks request goroutines; ConfigLogger is race-free;
  logWriterWorker unlock bug fixed
- stream writer panics recovered (process no longer crashes);
  streamDone closed exactly once
- generated Go client emits definitions for pointer-to-enum/struct fields
- anonymous structs rejected at registration; isPrivate safe on empty names
- removed dead Result.Id and RequestInfo.Type

ADDITIONS:
- graceful shutdown (Shutdown), StartOn, server timeouts by default
  (Read 60s/Idle 120s/Write off), SetTimeouts/SetMaxConcurrency
- big-int-safe JSON in the generated TS client (>2^53 as BigInt)
- example/ demo services and cmd/genexample artifact generator
This commit is contained in:
2026-09-03 15:32:07 +08:00
parent 9019798382
commit 41df889a1a
24 changed files with 1356 additions and 214 deletions
+49 -4
View File
@@ -3,12 +3,16 @@ package fun
import (
"errors"
"fmt"
"math"
"reflect"
"strings"
"unicode"
)
func isPrivate(value string) bool {
if value == "" {
return false // 匿名类型名:交由各处的具名校验给出明确报错,不在此越界 panic
}
return !unicode.IsUpper([]rune(value)[0])
}
@@ -36,6 +40,9 @@ func checkType(t reflect.Type) {
}
}
case reflect.Struct:
if t.Name() == "" {
panic("fun: anonymous struct types are not supported, define a named type")
}
if t.NumField() == 0 {
panic("fun: " + t.Name() + " must have at least one field")
}
@@ -49,7 +56,11 @@ func checkType(t reflect.Type) {
case reflect.Slice:
checkType(t.Elem())
default:
panic("fun:Unsupported types " + t.Name())
name := t.Name()
if name == "" {
name = t.String() // 匿名/内建组合类型(如 interface{})给出可读的名字
}
panic("fun:Unsupported types " + name)
}
}
@@ -122,7 +133,9 @@ func checkDto(dtoType reflect.Type, dtoMap any, methodName string) error {
return nil
}
// checkEnumValue 运行时校验枚举值是否在范围内
// checkEnumValue 运行时校验枚举值是否在范围内
// 必须在原始数值上先判范围再转 uint8:否则 256/512 等越界值先被截断成
// 合法小值(256→0),绕过范围检查后静默落到错误的枚举项上
func checkEnumValue(t reflect.Type, value any, name string) error {
var max uint8
enumValue := reflect.New(t).Elem()
@@ -131,35 +144,67 @@ func checkEnumValue(t reflect.Type, value any, name string) error {
} else {
max = uint8(len(enumValue.Interface().(enum).Names()))
}
outOfRange := callError(errors.New("Fun:" + name + " Dto value out of range"))
var num uint8
switch v := value.(type) {
case float64:
if v < 0 || v != math.Trunc(v) || v >= float64(max) {
return outOfRange
}
num = uint8(v)
case float32:
num = uint8(v)
f := float64(v)
if f < 0 || f != math.Trunc(f) || f >= float64(max) {
return outOfRange
}
num = uint8(f)
case uint8:
num = v
case uint16:
if v >= uint16(max) {
return outOfRange
}
num = uint8(v)
case uint32:
if v >= uint32(max) {
return outOfRange
}
num = uint8(v)
case uint64:
if v >= uint64(max) {
return outOfRange
}
num = uint8(v)
case int:
if v < 0 || v >= int(max) {
return outOfRange
}
num = uint8(v)
case int8:
if v < 0 || int(v) >= int(max) {
return outOfRange
}
num = uint8(v)
case int16:
if v < 0 || int(v) >= int(max) {
return outOfRange
}
num = uint8(v)
case int32:
if v < 0 || int64(v) >= int64(max) {
return outOfRange
}
num = uint8(v)
case int64:
if v < 0 || v >= int64(max) {
return outOfRange
}
num = uint8(v)
default:
return callError(errors.New("Fun:" + name + " Dto enum value type is not supported"))
}
if num >= max {
return callError(errors.New("Fun:" + name + " Dto value out of range"))
return outOfRange
}
return nil
}