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:
20
LICENSE
20
LICENSE
@@ -1,18 +1,18 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 trcreatives
|
Copyright (c) 2026 TR Creatives e.U.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
following conditions:
|
following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
portions of the Software.
|
portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|||||||
390
README.md
390
README.md
@@ -1,2 +1,392 @@
|
|||||||
# go-kite
|
# go-kite
|
||||||
|
|
||||||
|
A fast, lightweight, and expressive HTTP framework for Go, built on top of [httprouter](https://github.com/julienschmidt/httprouter).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Middleware** — global, group, and route-level middleware with correct onion ordering
|
||||||
|
- **Groups** — nestable route groups with prefix and middleware inheritance
|
||||||
|
- **Graceful shutdown** — in-flight requests finish cleanly on `SIGTERM` / `SIGINT`
|
||||||
|
- **Configurable** — server timeouts, error handlers, not-found handlers all configurable
|
||||||
|
- **Rich context** — typed request/response helpers, value store, cookie and form support
|
||||||
|
- **Built-in middleware** — logger, recovery, CORS, request ID, max body size
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.trcreatives.at/trcreatives/go-kite
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.trcreatives.at/trcreatives/go-kite"
|
||||||
|
"git.trcreatives.at/trcreatives/go-kite/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
k := kite.New()
|
||||||
|
|
||||||
|
k.Use(
|
||||||
|
middleware.Recovery(),
|
||||||
|
middleware.Logger(),
|
||||||
|
middleware.RequestID(),
|
||||||
|
)
|
||||||
|
|
||||||
|
k.GET("/", func(ctx *kite.Context) error {
|
||||||
|
return ctx.WriteJSON(http.StatusOK, map[string]string{
|
||||||
|
"message": "Hello, World!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := k.Start("localhost:8080"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
```go
|
||||||
|
k := kite.New()
|
||||||
|
|
||||||
|
k.GET("/users", listUsers)
|
||||||
|
k.POST("/users", createUser)
|
||||||
|
k.GET("/users/:id", getUser)
|
||||||
|
k.PUT("/users/:id", updateUser)
|
||||||
|
k.DELETE("/users/:id", deleteUser)
|
||||||
|
```
|
||||||
|
|
||||||
|
All standard HTTP methods are supported: `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `PATCH`.
|
||||||
|
|
||||||
|
## Route Groups
|
||||||
|
|
||||||
|
Groups allow you to share a common prefix and middleware across multiple routes.
|
||||||
|
|
||||||
|
```go
|
||||||
|
api := k.Group("/api")
|
||||||
|
api.GET("/users", listUsers) // GET /api/users
|
||||||
|
api.POST("/users", createUser) // POST /api/users
|
||||||
|
```
|
||||||
|
|
||||||
|
Groups can be nested:
|
||||||
|
|
||||||
|
```go
|
||||||
|
api := k.Group("/api")
|
||||||
|
v1 := api.Group("/v1")
|
||||||
|
v1.GET("/users", listUsers) // GET /api/v1/users
|
||||||
|
```
|
||||||
|
|
||||||
|
Middleware can be attached to groups:
|
||||||
|
|
||||||
|
```go
|
||||||
|
api := k.Group("/api", authMiddleware)
|
||||||
|
admin := api.Group("/admin", adminOnlyMiddleware)
|
||||||
|
admin.GET("/stats", getStats) // chain: global → auth → adminOnly → handler
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware
|
||||||
|
|
||||||
|
Middleware follows the standard onion model — each middleware wraps the next.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Middleware func(h Handler) Handler
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Middleware
|
||||||
|
|
||||||
|
Applied to every route:
|
||||||
|
|
||||||
|
```go
|
||||||
|
k.Use(middleware.Recovery())
|
||||||
|
k.Use(middleware.Logger())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Route Middleware
|
||||||
|
|
||||||
|
Applied to a single route:
|
||||||
|
|
||||||
|
```go
|
||||||
|
k.GET("/admin", adminHandler, authMiddleware, rateLimitMiddleware)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution Order
|
||||||
|
|
||||||
|
```
|
||||||
|
→ Global 1
|
||||||
|
→ Global 2
|
||||||
|
→ Group 1
|
||||||
|
→ Route 1
|
||||||
|
[Handler]
|
||||||
|
← Route 1
|
||||||
|
← Group 1
|
||||||
|
← Global 2
|
||||||
|
← Global 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing Custom Middleware
|
||||||
|
|
||||||
|
```go
|
||||||
|
func MyMiddleware(h kite.Handler) kite.Handler {
|
||||||
|
return func(ctx *kite.Context) error {
|
||||||
|
// before handler
|
||||||
|
err := h(ctx)
|
||||||
|
// after handler
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Middleware
|
||||||
|
|
||||||
|
All middleware lives in the `middleware` sub-package:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.trcreatives.at/trcreatives/go-kite/middleware"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logger
|
||||||
|
|
||||||
|
Logs method, path, status code and latency for every request.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// simple
|
||||||
|
k.Use(middleware.Logger())
|
||||||
|
|
||||||
|
// custom
|
||||||
|
k.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||||
|
Output: logFile,
|
||||||
|
Format: func(method, path string, statusCode int, latency time.Duration) string {
|
||||||
|
return fmt.Sprintf("%s %s %d %dms", method, path, statusCode, latency.Milliseconds())
|
||||||
|
},
|
||||||
|
Skip: func(ctx *kite.Context) bool {
|
||||||
|
return ctx.GetPath() == "/health"
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
|
||||||
|
Catches panics and returns a `500` instead of crashing the server.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// simple — always register this first
|
||||||
|
k.Use(middleware.Recovery())
|
||||||
|
|
||||||
|
// custom — integrate with Sentry, Datadog, etc.
|
||||||
|
k.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
|
||||||
|
OnPanic: func(ctx *kite.Context, recovered any, stack []byte) error {
|
||||||
|
sentry.CaptureException(fmt.Errorf("%v", recovered))
|
||||||
|
return ctx.WriteJSON(http.StatusInternalServerError, map[string]string{
|
||||||
|
"error": "internal server error",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### RequestID
|
||||||
|
|
||||||
|
Attaches a unique ID to every request. Reuses an incoming `X-Request-ID` header if present, for distributed tracing.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// simple
|
||||||
|
k.Use(middleware.RequestID())
|
||||||
|
|
||||||
|
// custom header
|
||||||
|
k.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{
|
||||||
|
Header: "X-Trace-ID",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// retrieve in handler
|
||||||
|
id := ctx.GetValue("X-Request-ID")
|
||||||
|
```
|
||||||
|
|
||||||
|
### CORS
|
||||||
|
|
||||||
|
Sets Cross-Origin Resource Sharing headers and handles preflight requests.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// allow all origins
|
||||||
|
k.Use(middleware.CORS())
|
||||||
|
|
||||||
|
// restrict origins
|
||||||
|
k.Use(middleware.CORS("https://myapp.com"))
|
||||||
|
|
||||||
|
// full control
|
||||||
|
k.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||||
|
AllowedOrigins: []string{"https://myapp.com"},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||||
|
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||||
|
ExposedHeaders: []string{"X-Request-ID"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 86400,
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** `AllowCredentials: true` cannot be combined with `AllowedOrigins: ["*"]` — browsers will reject it.
|
||||||
|
|
||||||
|
### MaxBodySize
|
||||||
|
|
||||||
|
Protects against oversized request bodies. Returns `413` if the limit is exceeded.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// global 4MB limit
|
||||||
|
k.Use(middleware.MaxBodySize(4 << 20))
|
||||||
|
|
||||||
|
// per-route override for file uploads
|
||||||
|
k.POST("/upload", uploadHandler, middleware.MaxBodySize(100 << 20))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Every handler receives a `*kite.Context` with the following methods:
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| ---------------------------------------------- | ------------------------------------- |
|
||||||
|
| `GetRequest() *http.Request` | Underlying request |
|
||||||
|
| `GetResponse() http.ResponseWriter` | Underlying response writer |
|
||||||
|
| `GetContext() context.Context` | Underlying context |
|
||||||
|
| `GetMethod() string` | HTTP method |
|
||||||
|
| `GetPath() string` | Request path |
|
||||||
|
| `IsMethod(method string) bool` | Check HTTP method |
|
||||||
|
| `GetPathParam(key string) string` | Route parameter (e.g. `/users/:id`) |
|
||||||
|
| `GetQueryParam(key string) string` | Query string parameter |
|
||||||
|
| `GetHeader(key string) string` | Request header |
|
||||||
|
| `GetIP() string` | Client IP, respects `X-Forwarded-For` |
|
||||||
|
| `GetCookie(name string) (*http.Cookie, error)` | Request cookie |
|
||||||
|
| `GetStatusCode() int` | Current response status code |
|
||||||
|
|
||||||
|
### Values
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| ----------------------------- | ---------------------------- |
|
||||||
|
| `SetValue(key string, v any)` | Store a value in the context |
|
||||||
|
| `GetValue(key string) any` | Retrieve a stored value |
|
||||||
|
|
||||||
|
### Forms
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| --------------------------------------------------------------------------------- | ----------------- |
|
||||||
|
| `GetForm() (url.Values, error)` | All form values |
|
||||||
|
| `GetFormValue(key string) string` | Single form value |
|
||||||
|
| `GetMultipartForm(maxMemory int64) (*multipart.Form, error)` | Multipart form |
|
||||||
|
| `GetMultipartFormFile(key string) (multipart.File, *multipart.FileHeader, error)` | Uploaded file |
|
||||||
|
|
||||||
|
### Body Binding
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| ----------------------- | ------------------------ |
|
||||||
|
| `BindJSON(v any) error` | Decode JSON request body |
|
||||||
|
| `BindXML(v any) error` | Decode XML request body |
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| ----------------------------------------- | ------------------- |
|
||||||
|
| `WriteBytes(status int, v []byte) error` | Raw bytes response |
|
||||||
|
| `WriteString(status int, v string) error` | Plain text response |
|
||||||
|
| `WriteJSON(status int, v any) error` | JSON response |
|
||||||
|
| `WriteXML(status int, v any) error` | XML response |
|
||||||
|
| `WriteNoContent() error` | 204 No Content |
|
||||||
|
| `Redirect(status int, url string) error` | Redirect |
|
||||||
|
| `SetHeader(key, value string)` | Set response header |
|
||||||
|
| `SetCookie(cookie *http.Cookie)` | Set response cookie |
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Handlers return `error`. By default, errors are logged and a `500` is returned to the client.
|
||||||
|
|
||||||
|
```go
|
||||||
|
k.GET("/users/:id", func(ctx *kite.Context) error {
|
||||||
|
user, err := db.GetUser(ctx.GetPathParam("id"))
|
||||||
|
if err != nil {
|
||||||
|
return err // caught by error handler
|
||||||
|
}
|
||||||
|
return ctx.WriteJSON(http.StatusOK, user)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Error Handler
|
||||||
|
|
||||||
|
```go
|
||||||
|
k.SetErrorHandler(func(ctx *kite.Context, err error) error {
|
||||||
|
return ctx.WriteJSON(http.StatusInternalServerError, map[string]string{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Not Found Handler
|
||||||
|
|
||||||
|
```go
|
||||||
|
k.SetNotFoundHandler(func(ctx *kite.Context) error {
|
||||||
|
return ctx.WriteJSON(http.StatusNotFound, map[string]string{
|
||||||
|
"error": "route not found",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Method Not Allowed Handler
|
||||||
|
|
||||||
|
```go
|
||||||
|
k.SetMethodNotAllowedHandler(func(ctx *kite.Context) error {
|
||||||
|
return ctx.WriteJSON(http.StatusMethodNotAllowed, map[string]string{
|
||||||
|
"error": "method not allowed",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
k := kite.New()
|
||||||
|
|
||||||
|
k.SetServerReadTimeout(10 * time.Second) // default: 10s
|
||||||
|
k.SetServerWriteTimeout(30 * time.Second) // default: 30s
|
||||||
|
k.SetServerIdleTimeout(60 * time.Second) // default: 60s
|
||||||
|
k.SetServerShutdownTimeout(30 * time.Second) // default: 30s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Graceful Shutdown
|
||||||
|
|
||||||
|
`Start()` blocks until `SIGINT` or `SIGTERM` is received, then waits for all in-flight requests to finish before exiting. This maps directly to how Docker, Kubernetes, and systemd stop processes.
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := k.Start("localhost:8080"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TLS
|
||||||
|
|
||||||
|
Use `StartTLS` instead of `Start` and provide your certificate and key files:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := k.StartTLS("localhost:443", "cert.pem", "key.pem"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For local development you can generate a self-signed certificate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
For production, use a certificate from a trusted CA. [Caddy](https://caddyserver.com) and [Nginx](https://nginx.org) are both good options as a reverse proxy if you prefer to terminate TLS outside the application.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
> **Coming Soon** — In-depth benchmarks including requests/sec, avg latency, RAM usage, and CPU utilization across varying concurrency levels, with comparisons against other Go frameworks.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
165
context.go
Normal file
165
context.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package kite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"mime/multipart"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
ctx context.Context
|
||||||
|
w *responseWriter
|
||||||
|
r *http.Request
|
||||||
|
p httprouter.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetRequest() *http.Request {
|
||||||
|
return c.r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetResponse() http.ResponseWriter {
|
||||||
|
return c.w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetStatusCode() int {
|
||||||
|
return c.w.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetContext() context.Context {
|
||||||
|
return c.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetPathParam(key string) string {
|
||||||
|
return c.p.ByName(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetQueryParam(key string) string {
|
||||||
|
return c.r.URL.Query().Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetValue(key string, v any) {
|
||||||
|
c.ctx = context.WithValue(c.ctx, contextKey(key), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetValue(key string) any {
|
||||||
|
return c.ctx.Value(contextKey(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetHeader(key, v string) {
|
||||||
|
c.w.Header().Set(key, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetHeader(key string) string {
|
||||||
|
return c.r.Header.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetCookie(cookie *http.Cookie) {
|
||||||
|
http.SetCookie(c.w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetCookie(key string) (*http.Cookie, error) {
|
||||||
|
return c.r.Cookie(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetIP() string {
|
||||||
|
if ip := c.r.Header.Get("X-Forwarded-For"); ip != "" {
|
||||||
|
return strings.Split(ip, ",")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := c.r.Header.Get("X-Real-IP"); ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, _, _ := net.SplitHostPort(c.r.RemoteAddr)
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetMethod() string {
|
||||||
|
return c.r.Method
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetPath() string {
|
||||||
|
return c.r.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetForm() (url.Values, error) {
|
||||||
|
if err := c.r.ParseForm(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.r.Form, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetFormValue(key string) string {
|
||||||
|
return c.r.FormValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetMultipartForm(maxMemory int64) (*multipart.Form, error) {
|
||||||
|
if err := c.r.ParseMultipartForm(maxMemory); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.r.MultipartForm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetMultipartFormFile(key string) (multipart.File, *multipart.FileHeader, error) {
|
||||||
|
return c.r.FormFile(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) IsMethod(method string) bool {
|
||||||
|
return c.r.Method == method
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WriteBytes(statusCode int, v []byte) error {
|
||||||
|
c.w.WriteHeader(statusCode)
|
||||||
|
_, err := c.w.Write(v)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WriteString(statusCode int, v string) error {
|
||||||
|
c.w.Header().Set("Content-Type", "text/plain")
|
||||||
|
c.w.WriteHeader(statusCode)
|
||||||
|
_, err := c.w.Write([]byte(v))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WriteJSON(statusCode int, v any) error {
|
||||||
|
c.w.Header().Set("Content-Type", "application/json")
|
||||||
|
c.w.WriteHeader(statusCode)
|
||||||
|
return json.NewEncoder(c.w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WriteXML(statusCode int, v any) error {
|
||||||
|
c.w.Header().Set("Content-Type", "application/xml")
|
||||||
|
c.w.WriteHeader(statusCode)
|
||||||
|
return xml.NewEncoder(c.w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WriteNoContent() error {
|
||||||
|
c.w.WriteHeader(http.StatusNoContent)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Redirect(statusCode int, url string) error {
|
||||||
|
http.Redirect(c.w, c.r, url, statusCode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) BindJSON(v any) error {
|
||||||
|
defer c.r.Body.Close()
|
||||||
|
return json.NewDecoder(c.r.Body).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) BindXML(v any) error {
|
||||||
|
defer c.r.Body.Close()
|
||||||
|
return xml.NewDecoder(c.r.Body).Decode(v)
|
||||||
|
}
|
||||||
9
error_handler.go
Normal file
9
error_handler.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package kite
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type ErrorHandler func(ctx *Context, err error) error
|
||||||
|
|
||||||
|
func defaultErrorHandler(ctx *Context, err error) error {
|
||||||
|
return ctx.WriteString(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module git.trcreatives.at/trcreatives/go-kite
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require github.com/julienschmidt/httprouter v1.3.0
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
57
group.go
Normal file
57
group.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package kite
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
k *Kite
|
||||||
|
prefix string
|
||||||
|
mws []Middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Group(prefix string, mws ...Middleware) *Group {
|
||||||
|
return &Group{
|
||||||
|
k: g.k,
|
||||||
|
prefix: g.prefix + prefix,
|
||||||
|
mws: append(append([]Middleware{}, g.mws...), mws...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Use(mws ...Middleware) {
|
||||||
|
g.mws = append(g.mws, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) CONNECT(path string, h Handler, mws ...Middleware) {
|
||||||
|
g.handle(http.MethodConnect, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) DELETE(path string, h Handler, mws ...Middleware) {
|
||||||
|
g.handle(http.MethodDelete, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) GET(path string, h Handler, mws ...Middleware) {
|
||||||
|
g.handle(http.MethodGet, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) HEAD(path string, h Handler, mws ...Middleware) {
|
||||||
|
g.handle(http.MethodHead, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) OPTIONS(path string, h Handler, mws ...Middleware) {
|
||||||
|
g.handle(http.MethodOptions, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) POST(path string, h Handler, mws ...Middleware) {
|
||||||
|
g.handle(http.MethodPost, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) PUT(path string, h Handler, mws ...Middleware) {
|
||||||
|
g.handle(http.MethodPut, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) TRACE(path string, h Handler, mws ...Middleware) {
|
||||||
|
g.handle(http.MethodTrace, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) handle(method, path string, h Handler, mws ...Middleware) {
|
||||||
|
g.k.handle(method, g.prefix+path, h, append(g.mws, mws...)...)
|
||||||
|
}
|
||||||
3
handler.go
Normal file
3
handler.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package kite
|
||||||
|
|
||||||
|
type Handler func(ctx *Context) error
|
||||||
202
kite.go
Normal file
202
kite.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
package kite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Kite struct {
|
||||||
|
r *httprouter.Router
|
||||||
|
errorHandler ErrorHandler
|
||||||
|
mws []Middleware
|
||||||
|
|
||||||
|
serverReadTimeout time.Duration
|
||||||
|
serverWriteTimeout time.Duration
|
||||||
|
serverIdleTimeout time.Duration
|
||||||
|
serverShutdownTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Kite {
|
||||||
|
k := &Kite{
|
||||||
|
r: httprouter.New(),
|
||||||
|
|
||||||
|
serverReadTimeout: 10 * time.Second,
|
||||||
|
serverWriteTimeout: 30 * time.Second,
|
||||||
|
serverIdleTimeout: 60 * time.Second,
|
||||||
|
serverShutdownTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
k.SetErrorHandler(defaultErrorHandler)
|
||||||
|
k.SetNotFoundHandler(defaultNotFoundHandler)
|
||||||
|
k.SetMethodNotAllowedHandler(defaultMethodNotAllowedHandler)
|
||||||
|
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) SetErrorHandler(errorHandler ErrorHandler) {
|
||||||
|
k.errorHandler = errorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) SetNotFoundHandler(notFoundHandler NotFoundHandler) {
|
||||||
|
k.r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := &Context{ctx: r.Context(), w: newResponseWriter(w), r: r}
|
||||||
|
notFoundHandler(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) SetMethodNotAllowedHandler(methodNotAllowedHandler MethodNotAllowedHandler) {
|
||||||
|
k.r.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := &Context{ctx: r.Context(), w: newResponseWriter(w), r: r}
|
||||||
|
methodNotAllowedHandler(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) SetServerReadTimeout(d time.Duration) {
|
||||||
|
k.serverReadTimeout = d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) SetServerWriteTimeout(d time.Duration) {
|
||||||
|
k.serverWriteTimeout = d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) SetServerIdleTimeout(d time.Duration) {
|
||||||
|
k.serverIdleTimeout = d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) SetServerShutdownTimeout(d time.Duration) {
|
||||||
|
k.serverShutdownTimeout = d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) Start(listenAddr string) error {
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: listenAddr,
|
||||||
|
Handler: k.r,
|
||||||
|
|
||||||
|
ReadTimeout: k.serverReadTimeout,
|
||||||
|
WriteTimeout: k.serverWriteTimeout,
|
||||||
|
IdleTimeout: k.serverIdleTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Printf("[Kite] Server error: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("[Kite] Server started and listening on http://%s\n", listenAddr)
|
||||||
|
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
log.Println("[Kite] Shutting down...")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), k.serverShutdownTimeout)
|
||||||
|
defer cancel()
|
||||||
|
return srv.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) StartTLS(listenAddr, certFile, keyFile string) error {
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: listenAddr,
|
||||||
|
Handler: k.r,
|
||||||
|
|
||||||
|
ReadTimeout: k.serverReadTimeout,
|
||||||
|
WriteTimeout: k.serverWriteTimeout,
|
||||||
|
IdleTimeout: k.serverIdleTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.ListenAndServeTLS(certFile, keyFile); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Printf("[Kite] Server error: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("[Kite] Server started and listening on https://%s\n", listenAddr)
|
||||||
|
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
log.Println("[Kite] Shutting down...")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), k.serverShutdownTimeout)
|
||||||
|
defer cancel()
|
||||||
|
return srv.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) Group(prefix string, mws ...Middleware) *Group {
|
||||||
|
return &Group{
|
||||||
|
k: k,
|
||||||
|
prefix: prefix,
|
||||||
|
mws: mws,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) Use(mws ...Middleware) {
|
||||||
|
k.mws = append(k.mws, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) CONNECT(path string, h Handler, mws ...Middleware) {
|
||||||
|
k.handle(http.MethodConnect, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) DELETE(path string, h Handler, mws ...Middleware) {
|
||||||
|
k.handle(http.MethodDelete, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) GET(path string, h Handler, mws ...Middleware) {
|
||||||
|
k.handle(http.MethodGet, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) HEAD(path string, h Handler, mws ...Middleware) {
|
||||||
|
k.handle(http.MethodHead, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) OPTIONS(path string, h Handler, mws ...Middleware) {
|
||||||
|
k.handle(http.MethodOptions, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) POST(path string, h Handler, mws ...Middleware) {
|
||||||
|
k.handle(http.MethodPost, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) PUT(path string, h Handler, mws ...Middleware) {
|
||||||
|
k.handle(http.MethodPut, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) TRACE(path string, h Handler, mws ...Middleware) {
|
||||||
|
k.handle(http.MethodTrace, path, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kite) handle(method, path string, h Handler, mws ...Middleware) {
|
||||||
|
k.r.Handle(method, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
ctx := &Context{
|
||||||
|
ctx: r.Context(),
|
||||||
|
w: newResponseWriter(w),
|
||||||
|
r: r,
|
||||||
|
p: p,
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedHandler := h
|
||||||
|
|
||||||
|
for i := len(mws) - 1; i >= 0; i-- {
|
||||||
|
wrappedHandler = mws[i](wrappedHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(k.mws) - 1; i >= 0; i-- {
|
||||||
|
wrappedHandler = k.mws[i](wrappedHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wrappedHandler(ctx); err != nil {
|
||||||
|
if err := k.errorHandler(ctx, err); err != nil {
|
||||||
|
log.Printf("[Kite] Error handler failed: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
9
method_not_allowed_handler.go
Normal file
9
method_not_allowed_handler.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package kite
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type MethodNotAllowedHandler Handler
|
||||||
|
|
||||||
|
func defaultMethodNotAllowedHandler(ctx *Context) error {
|
||||||
|
return ctx.WriteString(http.StatusMethodNotAllowed, "405 method not allowed")
|
||||||
|
}
|
||||||
3
middleware.go
Normal file
3
middleware.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package kite
|
||||||
|
|
||||||
|
type Middleware func(h Handler) Handler
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
not_found_handler.go
Normal file
9
not_found_handler.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package kite
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type NotFoundHandler Handler
|
||||||
|
|
||||||
|
func defaultNotFoundHandler(ctx *Context) error {
|
||||||
|
return ctx.WriteString(http.StatusNotFound, "404 not found")
|
||||||
|
}
|
||||||
33
response_writer.go
Normal file
33
response_writer.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package kite
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type responseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
statusCode int
|
||||||
|
written bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResponseWriter(w http.ResponseWriter) *responseWriter {
|
||||||
|
return &responseWriter{
|
||||||
|
ResponseWriter: w,
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *responseWriter) WriteHeader(statusCode int) {
|
||||||
|
if rw.written {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.statusCode = statusCode
|
||||||
|
rw.written = true
|
||||||
|
rw.ResponseWriter.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *responseWriter) Write(b []byte) (int, error) {
|
||||||
|
if !rw.written {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
return rw.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user