feat: initial implementation of go-kite
Core framework: - Kite router with full HTTP method support (GET, POST, PUT, DELETE, HEAD, OPTIONS, CONNECT, TRACE) - Nestable route groups with prefix and middleware inheritance - Global, group, and route-level middleware with onion ordering - Centralized error, not-found, and method-not-allowed handlers - Graceful shutdown on SIGTERM/SIGINT with configurable timeout - Configurable server read, write, and idle timeouts - Context with typed request/response helpers, value store, cookie, form, and body binding support - Response writer wrapper for status code tracking Middleware: - Logger with configurable output, format, and skip function - Recovery with configurable panic handler - RequestID with configurable header and generator, forwards incoming IDs - CORS with configurable origins, methods, headers, credentials, and max age - MaxBodySize with configurable byte limit Docs: - README with quickstart, routing, middleware, context API reference, and TLS guide
This commit is contained in:
67
middleware/cors.go
Normal file
67
middleware/cors.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.trcreatives.at/trcreatives/go-kite"
|
||||
)
|
||||
|
||||
type CORSConfig struct {
|
||||
AllowedOrigins []string
|
||||
AllowedMethods []string
|
||||
AllowedHeaders []string
|
||||
ExposedHeaders []string
|
||||
AllowCredentials bool
|
||||
MaxAge int
|
||||
}
|
||||
|
||||
func CORS(allowedOrigins ...string) kite.Middleware {
|
||||
return CORSWithConfig(CORSConfig{AllowedOrigins: allowedOrigins})
|
||||
}
|
||||
|
||||
func CORSWithConfig(cfg CORSConfig) kite.Middleware {
|
||||
if len(cfg.AllowedOrigins) == 0 {
|
||||
cfg.AllowedOrigins = []string{"*"}
|
||||
}
|
||||
|
||||
if len(cfg.AllowedMethods) == 0 {
|
||||
cfg.AllowedMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "CONNECT", "TRACE"}
|
||||
}
|
||||
|
||||
if len(cfg.AllowedHeaders) == 0 {
|
||||
cfg.AllowedHeaders = []string{"Content-Type", "Authorization"}
|
||||
}
|
||||
|
||||
origins := strings.Join(cfg.AllowedOrigins, ", ")
|
||||
methods := strings.Join(cfg.AllowedMethods, ", ")
|
||||
headers := strings.Join(cfg.AllowedHeaders, ", ")
|
||||
exposed := strings.Join(cfg.ExposedHeaders, ", ")
|
||||
|
||||
return func(h kite.Handler) kite.Handler {
|
||||
return func(ctx *kite.Context) error {
|
||||
ctx.SetHeader("Access-Control-Allow-Origin", origins)
|
||||
ctx.SetHeader("Access-Control-Allow-Methods", methods)
|
||||
ctx.SetHeader("Access-Control-Allow-Headers", headers)
|
||||
|
||||
if exposed != "" {
|
||||
ctx.SetHeader("Access-Control-Expose-Headers", exposed)
|
||||
}
|
||||
|
||||
if cfg.AllowCredentials {
|
||||
ctx.SetHeader("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
if cfg.MaxAge > 0 {
|
||||
ctx.SetHeader("Access-Control-Max-Age", fmt.Sprintf("%d", cfg.MaxAge))
|
||||
}
|
||||
|
||||
if ctx.IsMethod(http.MethodOptions) {
|
||||
return ctx.WriteNoContent()
|
||||
}
|
||||
|
||||
return h(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
51
middleware/logger.go
Normal file
51
middleware/logger.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.trcreatives.at/trcreatives/go-kite"
|
||||
)
|
||||
|
||||
type LoggerConfig struct {
|
||||
Output io.Writer
|
||||
Format func(method, path string, statusCode int, latency time.Duration) string
|
||||
Skip func(ctx *kite.Context) bool
|
||||
}
|
||||
|
||||
func DefaultLoggerFormat(method, path string, statusCode int, latency time.Duration) string {
|
||||
return fmt.Sprintf("[Kite] %s %s -> %d in %s", method, path, statusCode, latency)
|
||||
}
|
||||
|
||||
func Logger() kite.Middleware {
|
||||
return LoggerWithConfig(LoggerConfig{})
|
||||
}
|
||||
|
||||
func LoggerWithConfig(cfg LoggerConfig) kite.Middleware {
|
||||
if cfg.Output == nil {
|
||||
cfg.Output = os.Stdout
|
||||
}
|
||||
|
||||
if cfg.Format == nil {
|
||||
cfg.Format = DefaultLoggerFormat
|
||||
}
|
||||
|
||||
logger := log.New(cfg.Output, "", log.LstdFlags)
|
||||
|
||||
return func(h kite.Handler) kite.Handler {
|
||||
return func(ctx *kite.Context) error {
|
||||
if cfg.Skip != nil && cfg.Skip(ctx) {
|
||||
return h(ctx)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
err := h(ctx)
|
||||
logger.Println(cfg.Format(ctx.GetMethod(), ctx.GetPath(), ctx.GetStatusCode(), time.Since(start)))
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
28
middleware/max_body_size.go
Normal file
28
middleware/max_body_size.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.trcreatives.at/trcreatives/go-kite"
|
||||
)
|
||||
|
||||
type MaxBodySizeConfig struct {
|
||||
MaxBytes int64
|
||||
}
|
||||
|
||||
func MaxBodySize(maxBytes int64) kite.Middleware {
|
||||
return MaxBodySizeWithConfig(MaxBodySizeConfig{MaxBytes: maxBytes})
|
||||
}
|
||||
|
||||
func MaxBodySizeWithConfig(cfg MaxBodySizeConfig) kite.Middleware {
|
||||
if cfg.MaxBytes == 0 {
|
||||
cfg.MaxBytes = 4 << 20
|
||||
}
|
||||
|
||||
return func(h kite.Handler) kite.Handler {
|
||||
return func(ctx *kite.Context) error {
|
||||
ctx.GetRequest().Body = http.MaxBytesReader(ctx.GetResponse(), ctx.GetRequest().Body, cfg.MaxBytes)
|
||||
return h(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
37
middleware/recovery.go
Normal file
37
middleware/recovery.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"git.trcreatives.at/trcreatives/go-kite"
|
||||
)
|
||||
|
||||
type RecoveryConfig struct {
|
||||
OnPanic func(ctx *kite.Context, recovered any, stack []byte) error
|
||||
}
|
||||
|
||||
func Recovery() kite.Middleware {
|
||||
return RecoveryWithConfig(RecoveryConfig{})
|
||||
}
|
||||
|
||||
func RecoveryWithConfig(cfg RecoveryConfig) kite.Middleware {
|
||||
if cfg.OnPanic == nil {
|
||||
cfg.OnPanic = func(ctx *kite.Context, recovered any, stack []byte) error {
|
||||
log.Printf("[Kite] Panic: %v\n%s", recovered, stack)
|
||||
return ctx.WriteString(http.StatusInternalServerError, "500 internal server error")
|
||||
}
|
||||
}
|
||||
|
||||
return func(h kite.Handler) kite.Handler {
|
||||
return func(ctx *kite.Context) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = cfg.OnPanic(ctx, r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
return h(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
46
middleware/request_id.go
Normal file
46
middleware/request_id.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
"git.trcreatives.at/trcreatives/go-kite"
|
||||
)
|
||||
|
||||
type RequestIDConfig struct {
|
||||
Header string
|
||||
Generator func() string
|
||||
}
|
||||
|
||||
func RequestID() kite.Middleware {
|
||||
return RequestIDWithConfig(RequestIDConfig{})
|
||||
}
|
||||
|
||||
func RequestIDWithConfig(cfg RequestIDConfig) kite.Middleware {
|
||||
if cfg.Header == "" {
|
||||
cfg.Header = "X-Request-ID"
|
||||
}
|
||||
|
||||
if cfg.Generator == nil {
|
||||
cfg.Generator = func() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||
}
|
||||
}
|
||||
|
||||
return func(h kite.Handler) kite.Handler {
|
||||
return func(ctx *kite.Context) error {
|
||||
id := ctx.GetHeader(cfg.Header)
|
||||
if id == "" {
|
||||
id = cfg.Generator()
|
||||
}
|
||||
|
||||
ctx.SetHeader(cfg.Header, id)
|
||||
ctx.SetValue(cfg.Header, id)
|
||||
|
||||
return h(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user