This content originally appeared on DEV Community and was authored by Austin
What is Gonk?
This is a quick recap for those who haven’t read my first post about Gonk. Read part one here. If you don’t want to do so, then here is a copy paste from that blog post explaining what it is:
Gonk is a project that is part of a four part project that I have planned. Gonk itself is an HTTP Framework I am developing based on the stdlib net/http package from Go. It will include routing, middleware, JSON utility, and more.
So what has been done since part one?
Great question! Gonk has been fully built out to MVP at this point. I have a fully functional HTTP Framework that contains some basic routing, middleware, and JSON utility. In part one, I showed off the routing for GET requests. I have since added routing for POST, PATCH, PUT, DELETE, HEAD, and OPTIONS. I implemented middleware for CORS, server side logging, panic recovery, and request IDs.
Middleware
The Goal
I made my middleware fairly simple. I didn’t want to focus on getting overly complex, as this was a learning opportunity for me, and if I tried to recreate everything that a normal, popular framework has, I would have a harder time grasping the what was needed and why it was needed.
Request ID
I started off with X-Request-Id
as this is an essential part of logging. The general idea here was that every request would carry an X-Request-Id
. If the request already contained an X-Request-Id
then it would use that, else it would create one.
For creating a new Request ID, I decided to keep it simple and create the Request ID as a hex encoded string.
func newReqID() string {
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
return strconv.FormatInt(time.Now().UnixNano(), 16)
}
return hex.EncodeToString(buf)
}
I then needed a way to store a Request ID in context.
func storeReqID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
Finally I could create the ReqID method that can be used in my middleware chain:
func ReqID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Reads the incoming header, checks for ReqID
id := r.Header.Get("X-Request-Id")
// Checks the len of the incoming ID
// >128, creates a new ReqID
if len(strings.TrimSpace(id)) == 0 || len(id) > 128 {
id = newReqID()
}
// Make it visible to client and downstream middleware
w.Header().Set("X-Request-Id", id)
r = r.WithContext(storeReqID(r.Context(), id))
next.ServeHTTP(w, r)
})
}
Another important aspect is that I knew I would need a way to retrieve a Request ID from context. I implemented the following:
func ReqIDFromCtx(ctx context.Context) (string, bool) {
v := ctx.Value(requestIDKey)
s, ok := v.(string)
return s, ok
}
Logger
Now that I had a way to get Request IDs, the next thing I implemented was a logger. I wanted my logging to be returned as JSON, as this would be easy to work with, and it would also tie nicely into things like Elastic if I decided to implement a SIEM.
Each request should contain the following:
- Method
- Path
- Bytes Written
- Latency
X-Request-Id
Here’s a quick snippet of my Logger:
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(wrapped, r)
id, _ := ReqIDFromCtx(r.Context())
rec := map[string]any{
"ts": time.Now().Format(time.RFC3339Nano),
"level": "info",
"request_id": id,
"method": r.Method,
"path": r.URL.Path,
"status": wrapped.status,
"bytes": wrapped.bytes,
"latency_ms": time.Since(start).Milliseconds(),
"remote_ip": r.RemoteAddr,
"user_agent": r.UserAgent(),
}
_ = json.NewEncoder(os.Stdout).Encode(rec)
})
}
The result? Take a look for yourself:
Panic Recovery
This is definitely an important aspect to my middleware chain. I am certainly not a perfect dev, and I am bound to make mistakes. I didn’t want a panic to take the whole server down. The Recovery middleware wraps everything at the top and ensures that if a handler blows up, it gets caught. It logs the panic (with stack trace for my eyes only) and responds to the client with a clean 500 Internal Server Error
JSON body. No stack traces leaking over HTTP, no server crashes.
I did, however make a route for this for testing purposes. Here’s the result:
CORS
The final piece of the middleware puzzle for me was CORS. This is obviously a very important aspect in web development. If you have a frontend, you’re going to have to deal with CORS at some point. I made this configurable. You can allow all origins for dev, or lock it down to specific origins in production. It handles both simple requests and preflight OPTIONS
request. The key was keeping it safe, no mixing "*"
with Allow-Credentials: true
, since browsers forbid it. This way I don’t accidentally open a security hole.
JSON Utility
Now that I had a working middleware chain, I wanted to add one more piece of functionality to my HTTP Framework, and that is JSON utility. It’s very common to deal with JSON in web development, and Go is notoriously verbose when dealing with everything JSON.
I built utilities for the following:
-
WriteJSON
: Encodes data, sets the right headers, writes status. -
WriteError
: Consistent error shape — always{"error":"..."}
. -
DecodeJSON
: Strict decode. Rejects unknown fields, empty bodies, multiple JSON values. Returns typed errors so I can map them to status codes. -
StatusFromDecodeError
: Converts decode errors into proper HTTP codes (400 for bad input, 413 for too large, 422 for type mismatches, etc.).
Design Choice
I designed this HTTP framework to be simple to use, have a little bit of ergonomic utility, and mostly for the sake of learning. I used the stdlib net/http
wherever I could, including http.ServeMux
. This allowed for relatively fast development, and I am overall happy with my end product. I absolutely learned a lot about how Go handles HTTP requests and how the stdlib net/http
works.
Final Thoughts
Again, I really enjoyed this project. Doing this really solidified my enjoyment in choosing to learn Go. Things are really starting to click with me personally on a lower level. Most of my dev experience has been with Javascript in the past. I always understood how to do something, but I really never fully grasped the why outside of a high level. Choosing Go has forced me to learn the why, but it’s also made this very fun in the process.
If I were to go back or revisit this project, I would say that I would probably not use http.ServeMux
. Not because it didn’t fit my use case, but actually because it fit it too well. This was a great learning opportunity for me, and I would love to do a “2.0” version of Gonk. To even further solidify my understand of TCP, HTTP, and network code, I would love to go back and do more from scratch. That means creating my own http.ServeMux
and potentially not even use the net/http
at all. Or, limited use of it at least. I find I learn best by doing, and that would be a way to learn even more. Either way, I do not have regrets for how I build Gonk.
Now we’re on to the Data Store as part two of my four part project!
This content originally appeared on DEV Community and was authored by Austin